diff options
| author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2024-06-20 22:43:06 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-06-20 22:43:06 +0200 |
| commit | c1a64c1b75f97cef97965b3bd9a941564a6270bd (patch) | |
| tree | b9a92dff6462abd5859c3c76f19748fad5d6c025 /examples | |
| parent | 47c24eed0191f8f72646be63dee94ac2b35eb062 (diff) | |
| parent | b87e6d6d762ee823e81dd7a8984f330eb4018fd8 (diff) | |
| download | luasystem-c1a64c1b75f97cef97965b3bd9a941564a6270bd.tar.gz luasystem-c1a64c1b75f97cef97965b3bd9a941564a6270bd.tar.bz2 luasystem-c1a64c1b75f97cef97965b3bd9a941564a6270bd.zip | |
Merge pull request #21 from lunarmodules/terminal
Diffstat (limited to 'examples')
| -rw-r--r-- | examples/compat.lua | 40 | ||||
| -rw-r--r-- | examples/flag_debugging.lua | 7 | ||||
| -rw-r--r-- | examples/password_input.lua | 59 | ||||
| -rw-r--r-- | examples/read.lua | 70 | ||||
| -rw-r--r-- | examples/readline.lua | 472 | ||||
| -rw-r--r-- | examples/spinner.lua | 64 | ||||
| -rw-r--r-- | examples/spiral_snake.lua | 72 | ||||
| -rw-r--r-- | examples/terminalsize.lua | 37 |
8 files changed, 821 insertions, 0 deletions
diff --git a/examples/compat.lua b/examples/compat.lua new file mode 100644 index 0000000..c712105 --- /dev/null +++ b/examples/compat.lua | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | -- This example shows how to remove platform differences to create a | ||
| 2 | -- cross-platform level playing field. | ||
| 3 | |||
| 4 | local sys = require "system" | ||
| 5 | |||
| 6 | |||
| 7 | |||
| 8 | if sys.windows then | ||
| 9 | -- Windows holds multiple copies of environment variables, to ensure `getenv` | ||
| 10 | -- returns what `setenv` sets we need to use the `system.getenv` instead of | ||
| 11 | -- `os.getenv`. | ||
| 12 | os.getenv = sys.getenv -- luacheck: ignore | ||
| 13 | |||
| 14 | -- Set console output to UTF-8 encoding. | ||
| 15 | sys.setconsoleoutputcp(sys.CODEPAGE_UTF8) | ||
| 16 | |||
| 17 | -- Set up the terminal to handle ANSI escape sequences on Windows. | ||
| 18 | if sys.isatty(io.stdout) then | ||
| 19 | sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
| 20 | end | ||
| 21 | if sys.isatty(io.stderr) then | ||
| 22 | sys.setconsoleflags(io.stderr, sys.getconsoleflags(io.stderr) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
| 23 | end | ||
| 24 | if sys.isatty(io.stdin) then | ||
| 25 | sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdout) + sys.ENABLE_VIRTUAL_TERMINAL_INPUT) | ||
| 26 | end | ||
| 27 | |||
| 28 | |||
| 29 | else | ||
| 30 | -- On Posix, one can set a variable to an empty string, but on Windows, this | ||
| 31 | -- will remove the variable from the environment. To make this consistent | ||
| 32 | -- across platforms, we will remove the variable from the environment if the | ||
| 33 | -- value is an empty string. | ||
| 34 | local old_setenv = sys.setenv | ||
| 35 | function sys.setenv(name, value) | ||
| 36 | if value == "" then value = nil end | ||
| 37 | return old_setenv(name, value) | ||
| 38 | end | ||
| 39 | end | ||
| 40 | |||
diff --git a/examples/flag_debugging.lua b/examples/flag_debugging.lua new file mode 100644 index 0000000..5f1d496 --- /dev/null +++ b/examples/flag_debugging.lua | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | local sys = require "system" | ||
| 2 | |||
| 3 | -- Print the Windows Console flags for stdin | ||
| 4 | sys.listconsoleflags(io.stdin) | ||
| 5 | |||
| 6 | -- Print the Posix termios flags for stdin | ||
| 7 | sys.listtermflags(io.stdin) | ||
diff --git a/examples/password_input.lua b/examples/password_input.lua new file mode 100644 index 0000000..2994062 --- /dev/null +++ b/examples/password_input.lua | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | local sys = require "system" | ||
| 2 | |||
| 3 | print [[ | ||
| 4 | |||
| 5 | This example shows how to disable the "echo" of characters read to the console, | ||
| 6 | useful for reading secrets from the user. | ||
| 7 | |||
| 8 | ]] | ||
| 9 | |||
| 10 | --- Function to read from stdin without echoing the input (for secrets etc). | ||
| 11 | -- It will (in a platform agnostic way) disable echo on the terminal, read the | ||
| 12 | -- input, and then re-enable echo. | ||
| 13 | -- @param ... Arguments to pass to `io.stdin:read()` | ||
| 14 | -- @return the results of `io.stdin:read(...)` | ||
| 15 | local function read_secret(...) | ||
| 16 | local w_oldflags, p_oldflags | ||
| 17 | |||
| 18 | if sys.isatty(io.stdin) then | ||
| 19 | -- backup settings, configure echo flags | ||
| 20 | w_oldflags = sys.getconsoleflags(io.stdin) | ||
| 21 | p_oldflags = sys.tcgetattr(io.stdin) | ||
| 22 | -- set echo off to not show password on screen | ||
| 23 | assert(sys.setconsoleflags(io.stdin, w_oldflags - sys.CIF_ECHO_INPUT)) | ||
| 24 | assert(sys.tcsetattr(io.stdin, sys.TCSANOW, { lflag = p_oldflags.lflag - sys.L_ECHO })) | ||
| 25 | end | ||
| 26 | |||
| 27 | local secret, err = io.stdin:read(...) | ||
| 28 | |||
| 29 | -- restore settings | ||
| 30 | if sys.isatty(io.stdin) then | ||
| 31 | io.stdout:write("\n") -- Add newline after reading the password | ||
| 32 | sys.setconsoleflags(io.stdin, w_oldflags) | ||
| 33 | sys.tcsetattr(io.stdin, sys.TCSANOW, p_oldflags) | ||
| 34 | end | ||
| 35 | |||
| 36 | return secret, err | ||
| 37 | end | ||
| 38 | |||
| 39 | |||
| 40 | |||
| 41 | -- Get username | ||
| 42 | io.write("Username: ") | ||
| 43 | local username = io.stdin:read("*l") | ||
| 44 | |||
| 45 | -- Get the secret | ||
| 46 | io.write("Password: ") | ||
| 47 | local password = read_secret("*l") | ||
| 48 | |||
| 49 | -- Get domainname | ||
| 50 | io.write("Domain : ") | ||
| 51 | local domain = io.stdin:read("*l") | ||
| 52 | |||
| 53 | |||
| 54 | -- Print the results | ||
| 55 | print("") | ||
| 56 | print("Here's what we got:") | ||
| 57 | print(" username: " .. username) | ||
| 58 | print(" password: " .. password) | ||
| 59 | print(" domain : " .. domain) | ||
diff --git a/examples/read.lua b/examples/read.lua new file mode 100644 index 0000000..4b57b54 --- /dev/null +++ b/examples/read.lua | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | local sys = require "system" | ||
| 2 | |||
| 3 | print [[ | ||
| 4 | |||
| 5 | This example shows how to do a non-blocking read from the cli. | ||
| 6 | |||
| 7 | ]] | ||
| 8 | |||
| 9 | -- setup Windows console to handle ANSI processing | ||
| 10 | local of_in = sys.getconsoleflags(io.stdin) | ||
| 11 | local of_out = sys.getconsoleflags(io.stdout) | ||
| 12 | sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
| 13 | sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT) | ||
| 14 | |||
| 15 | -- setup Posix terminal to use non-blocking mode, and disable line-mode | ||
| 16 | local of_attr = sys.tcgetattr(io.stdin) | ||
| 17 | local of_block = sys.getnonblock(io.stdin) | ||
| 18 | sys.setnonblock(io.stdin, true) | ||
| 19 | sys.tcsetattr(io.stdin, sys.TCSANOW, { | ||
| 20 | lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, -- disable canonical mode and echo | ||
| 21 | }) | ||
| 22 | |||
| 23 | -- cursor sequences | ||
| 24 | local get_cursor_pos = "\27[6n" | ||
| 25 | |||
| 26 | |||
| 27 | |||
| 28 | print("Press a key, or 'A' to get cursor position, 'ESC' to exit") | ||
| 29 | while true do | ||
| 30 | local key, keytype | ||
| 31 | |||
| 32 | -- wait for a key | ||
| 33 | while not key do | ||
| 34 | key, keytype = sys.readansi(math.huge) | ||
| 35 | end | ||
| 36 | |||
| 37 | if key == "A" then io.write(get_cursor_pos); io.flush() end | ||
| 38 | |||
| 39 | -- check if we got a key or ANSI sequence | ||
| 40 | if keytype == "char" then | ||
| 41 | -- just a key | ||
| 42 | local b = key:byte() | ||
| 43 | if b < 32 then | ||
| 44 | key = "." -- replace control characters with a simple "." to not mess up the screen | ||
| 45 | end | ||
| 46 | |||
| 47 | print("you pressed: " .. key .. " (" .. b .. ")") | ||
| 48 | if b == 27 then | ||
| 49 | print("Escape pressed, exiting") | ||
| 50 | break | ||
| 51 | end | ||
| 52 | |||
| 53 | elseif keytype == "ansi" then | ||
| 54 | -- we got an ANSI sequence | ||
| 55 | local seq = { key:byte(1, #key) } | ||
| 56 | print("ANSI sequence received: " .. key:sub(2,-1), "(bytes: " .. table.concat(seq, ", ")..")") | ||
| 57 | |||
| 58 | else | ||
| 59 | print("unknown key type received: " .. tostring(keytype)) | ||
| 60 | end | ||
| 61 | end | ||
| 62 | |||
| 63 | |||
| 64 | |||
| 65 | -- Clean up afterwards | ||
| 66 | sys.setnonblock(io.stdin, false) | ||
| 67 | sys.setconsoleflags(io.stdout, of_out) | ||
| 68 | sys.setconsoleflags(io.stdin, of_in) | ||
| 69 | sys.tcsetattr(io.stdin, sys.TCSANOW, of_attr) | ||
| 70 | sys.setnonblock(io.stdin, of_block) | ||
diff --git a/examples/readline.lua b/examples/readline.lua new file mode 100644 index 0000000..ff215dd --- /dev/null +++ b/examples/readline.lua | |||
| @@ -0,0 +1,472 @@ | |||
| 1 | --- An example class for reading a line of input from the user in a non-blocking way. | ||
| 2 | -- It uses ANSI escape sequences to move the cursor and handle input. | ||
| 3 | -- It can be used to read a line of input from the user, with a prompt. | ||
| 4 | -- It can handle double-width UTF-8 characters. | ||
| 5 | -- It can be used asynchroneously if `system.sleep` is patched to yield to a coroutine scheduler. | ||
| 6 | |||
| 7 | local sys = require("system") | ||
| 8 | |||
| 9 | |||
| 10 | -- Mapping of key-sequences to key-names | ||
| 11 | local key_names = { | ||
| 12 | ["\27[C"] = "right", | ||
| 13 | ["\27[D"] = "left", | ||
| 14 | ["\127"] = "backspace", | ||
| 15 | ["\27[3~"] = "delete", | ||
| 16 | ["\27[H"] = "home", | ||
| 17 | ["\27[F"] = "end", | ||
| 18 | ["\27"] = "escape", | ||
| 19 | ["\9"] = "tab", | ||
| 20 | ["\27[Z"] = "shift-tab", | ||
| 21 | } | ||
| 22 | |||
| 23 | if sys.windows then | ||
| 24 | key_names["\13"] = "enter" | ||
| 25 | else | ||
| 26 | key_names["\10"] = "enter" | ||
| 27 | end | ||
| 28 | |||
| 29 | |||
| 30 | -- Mapping of key-names to key-sequences | ||
| 31 | local key_sequences = {} | ||
| 32 | for k, v in pairs(key_names) do | ||
| 33 | key_sequences[v] = k | ||
| 34 | end | ||
| 35 | |||
| 36 | |||
| 37 | -- bell character | ||
| 38 | local function bell() | ||
| 39 | io.write("\7") | ||
| 40 | io.flush() | ||
| 41 | end | ||
| 42 | |||
| 43 | |||
| 44 | -- generate string to move cursor horizontally | ||
| 45 | -- positive goes right, negative goes left | ||
| 46 | local function cursor_move_horiz(n) | ||
| 47 | if n == 0 then | ||
| 48 | return "" | ||
| 49 | end | ||
| 50 | return "\27[" .. (n > 0 and n or -n) .. (n > 0 and "C" or "D") | ||
| 51 | end | ||
| 52 | |||
| 53 | |||
| 54 | -- -- generate string to move cursor vertically | ||
| 55 | -- -- positive goes down, negative goes up | ||
| 56 | -- local function cursor_move_vert(n) | ||
| 57 | -- if n == 0 then | ||
| 58 | -- return "" | ||
| 59 | -- end | ||
| 60 | -- return "\27[" .. (n > 0 and n or -n) .. (n > 0 and "B" or "A") | ||
| 61 | -- end | ||
| 62 | |||
| 63 | |||
| 64 | -- -- log to the line above the current line | ||
| 65 | -- local function log(...) | ||
| 66 | -- local arg = { n = select("#", ...), ...} | ||
| 67 | -- for i = 1, arg.n do | ||
| 68 | -- arg[i] = tostring(arg[i]) | ||
| 69 | -- end | ||
| 70 | -- arg = " " .. table.concat(arg, " ") .. " " | ||
| 71 | |||
| 72 | -- io.write(cursor_move_vert(-1), arg, cursor_move_vert(1), cursor_move_horiz(-#arg)) | ||
| 73 | -- end | ||
| 74 | |||
| 75 | |||
| 76 | -- UTF8 character size in bytes | ||
| 77 | -- @tparam number b the byte value of the first byte of a UTF8 character | ||
| 78 | local function utf8size(b) | ||
| 79 | return b < 128 and 1 or b < 224 and 2 or b < 240 and 3 or b < 248 and 4 | ||
| 80 | end | ||
| 81 | |||
| 82 | |||
| 83 | |||
| 84 | local utf8parse do | ||
| 85 | local utf8_value_mt = { | ||
| 86 | __tostring = function(self) | ||
| 87 | return table.concat(self, "") | ||
| 88 | end, | ||
| 89 | } | ||
| 90 | |||
| 91 | -- Parses a UTF8 string into list of individual characters. | ||
| 92 | -- key 'chars' gets the length in UTF8 characters, whilst # returns the length | ||
| 93 | -- for display (to handle double-width UTF8 chars). | ||
| 94 | -- in the list the double-width characters are followed by an empty string. | ||
| 95 | -- @tparam string s the UTF8 string to parse | ||
| 96 | -- @treturn table the list of characters | ||
| 97 | function utf8parse(s) | ||
| 98 | local t = setmetatable({ chars = 0 }, utf8_value_mt) | ||
| 99 | local i = 1 | ||
| 100 | while i <= #s do | ||
| 101 | local b = s:byte(i) | ||
| 102 | local w = utf8size(b) | ||
| 103 | local char = s:sub(i, i + w - 1) | ||
| 104 | t[#t + 1] = char | ||
| 105 | t.chars = t.chars + 1 | ||
| 106 | if sys.utf8cwidth(char) == 2 then | ||
| 107 | -- double width character, add empty string to keep the length of the | ||
| 108 | -- list the same as the character width on screen | ||
| 109 | t[#t + 1] = "" | ||
| 110 | end | ||
| 111 | i = i + w | ||
| 112 | end | ||
| 113 | return t | ||
| 114 | end | ||
| 115 | end | ||
| 116 | |||
| 117 | |||
| 118 | |||
| 119 | -- inline tests for utf8parse | ||
| 120 | -- do | ||
| 121 | -- local t = utf8parse("a你b好c") | ||
| 122 | -- assert(t[1] == "a") | ||
| 123 | -- assert(t[2] == "你") -- double width | ||
| 124 | -- assert(t[3] == "") | ||
| 125 | -- assert(t[4] == "b") | ||
| 126 | -- assert(t[5] == "好") -- double width | ||
| 127 | -- assert(t[6] == "") | ||
| 128 | -- assert(t[7] == "c") | ||
| 129 | -- assert(#t == 7) -- size as displayed | ||
| 130 | -- end | ||
| 131 | |||
| 132 | |||
| 133 | |||
| 134 | -- readline class | ||
| 135 | |||
| 136 | local readline = {} | ||
| 137 | readline.__index = readline | ||
| 138 | |||
| 139 | |||
| 140 | --- Create a new readline object. | ||
| 141 | -- @tparam table opts the options for the readline object | ||
| 142 | -- @tparam[opt=""] string opts.prompt the prompt to display | ||
| 143 | -- @tparam[opt=80] number opts.max_length the maximum length of the input (in characters, not bytes) | ||
| 144 | -- @tparam[opt=""] string opts.value the default value | ||
| 145 | -- @tparam[opt=`#value`] number opts.position of the cursor in the input | ||
| 146 | -- @tparam[opt={"\10"/"\13"}] table opts.exit_keys an array of keys that will cause the readline to exit | ||
| 147 | -- @treturn readline the new readline object | ||
| 148 | function readline.new(opts) | ||
| 149 | local value = utf8parse(opts.value or "") | ||
| 150 | local prompt = utf8parse(opts.prompt or "") | ||
| 151 | local pos = math.floor(opts.position or (#value + 1)) | ||
| 152 | pos = math.max(math.min(pos, (#value + 1)), 1) | ||
| 153 | local len = math.floor(opts.max_length or 80) | ||
| 154 | if len < 1 then | ||
| 155 | error("max_length must be at least 1", 2) | ||
| 156 | end | ||
| 157 | |||
| 158 | if value.chars > len then | ||
| 159 | error("value is longer than max_length", 2) | ||
| 160 | end | ||
| 161 | |||
| 162 | local exit_keys = {} | ||
| 163 | for _, key in ipairs(opts.exit_keys or {}) do | ||
| 164 | exit_keys[key] = true | ||
| 165 | end | ||
| 166 | if exit_keys[1] == nil then | ||
| 167 | -- nothing provided, default to Enter-key | ||
| 168 | exit_keys[1] = key_sequences.enter | ||
| 169 | end | ||
| 170 | |||
| 171 | local self = { | ||
| 172 | value = value, -- the default value | ||
| 173 | max_length = len, -- the maximum length of the input | ||
| 174 | prompt = prompt, -- the prompt to display | ||
| 175 | position = pos, -- the current position in the input | ||
| 176 | drawn_before = false, -- if the prompt has been drawn | ||
| 177 | exit_keys = exit_keys, -- the keys that will cause the readline to exit | ||
| 178 | } | ||
| 179 | |||
| 180 | setmetatable(self, readline) | ||
| 181 | return self | ||
| 182 | end | ||
| 183 | |||
| 184 | |||
| 185 | |||
| 186 | -- draw the prompt and the input value, and position the cursor. | ||
| 187 | local function draw(self, redraw) | ||
| 188 | if redraw or not self.drawn_before then | ||
| 189 | -- we are at start of prompt | ||
| 190 | self.drawn_before = true | ||
| 191 | else | ||
| 192 | -- we are at current cursor position, move to start of prompt | ||
| 193 | io.write(cursor_move_horiz(-(#self.prompt + self.position))) | ||
| 194 | end | ||
| 195 | -- write prompt & value | ||
| 196 | io.write(tostring(self.prompt) .. tostring(self.value)) | ||
| 197 | -- clear remainder of input size | ||
| 198 | io.write(string.rep(" ", self.max_length - self.value.chars)) | ||
| 199 | io.write(cursor_move_horiz(-(self.max_length - self.value.chars))) | ||
| 200 | -- move to cursor position | ||
| 201 | io.write(cursor_move_horiz(-(#self.value + 1 - self.position))) | ||
| 202 | io.flush() | ||
| 203 | end | ||
| 204 | |||
| 205 | |||
| 206 | local handle_key do -- keyboard input handler | ||
| 207 | |||
| 208 | local key_handlers | ||
| 209 | key_handlers = { | ||
| 210 | left = function(self) | ||
| 211 | if self.position == 1 then | ||
| 212 | bell() | ||
| 213 | return | ||
| 214 | end | ||
| 215 | |||
| 216 | local new_pos = self.position - 1 | ||
| 217 | while self.value[new_pos] == "" do -- skip empty strings; double width chars | ||
| 218 | new_pos = new_pos - 1 | ||
| 219 | end | ||
| 220 | |||
| 221 | io.write(cursor_move_horiz(-(self.position - new_pos))) | ||
| 222 | io.flush() | ||
| 223 | self.position = new_pos | ||
| 224 | end, | ||
| 225 | |||
| 226 | right = function(self) | ||
| 227 | if self.position == #self.value + 1 then | ||
| 228 | bell() | ||
| 229 | return | ||
| 230 | end | ||
| 231 | |||
| 232 | local new_pos = self.position + 1 | ||
| 233 | while self.value[new_pos] == "" do -- skip empty strings; double width chars | ||
| 234 | new_pos = new_pos + 1 | ||
| 235 | end | ||
| 236 | |||
| 237 | io.write(cursor_move_horiz(new_pos - self.position)) | ||
| 238 | io.flush() | ||
| 239 | self.position = new_pos | ||
| 240 | end, | ||
| 241 | |||
| 242 | backspace = function(self) | ||
| 243 | if self.position == 1 then | ||
| 244 | bell() | ||
| 245 | return | ||
| 246 | end | ||
| 247 | |||
| 248 | while self.value[self.position - 1] == "" do -- remove empty strings; double width chars | ||
| 249 | io.write(cursor_move_horiz(-1)) | ||
| 250 | self.position = self.position - 1 | ||
| 251 | table.remove(self.value, self.position) | ||
| 252 | end | ||
| 253 | -- remove char itself | ||
| 254 | io.write(cursor_move_horiz(-1)) | ||
| 255 | self.position = self.position - 1 | ||
| 256 | table.remove(self.value, self.position) | ||
| 257 | self.value.chars = self.value.chars - 1 | ||
| 258 | draw(self) | ||
| 259 | end, | ||
| 260 | |||
| 261 | home = function(self) | ||
| 262 | local new_pos = 1 | ||
| 263 | io.write(cursor_move_horiz(new_pos - self.position)) | ||
| 264 | self.position = new_pos | ||
| 265 | end, | ||
| 266 | |||
| 267 | ["end"] = function(self) | ||
| 268 | local new_pos = #self.value + 1 | ||
| 269 | io.write(cursor_move_horiz(new_pos - self.position)) | ||
| 270 | self.position = new_pos | ||
| 271 | end, | ||
| 272 | |||
| 273 | delete = function(self) | ||
| 274 | if self.position > #self.value then | ||
| 275 | bell() | ||
| 276 | return | ||
| 277 | end | ||
| 278 | |||
| 279 | key_handlers.right(self) | ||
| 280 | key_handlers.backspace(self) | ||
| 281 | end, | ||
| 282 | } | ||
| 283 | |||
| 284 | |||
| 285 | -- handles a single input key/ansi-sequence. | ||
| 286 | -- @tparam string key the key or ansi-sequence (from `system.readansi`) | ||
| 287 | -- @tparam string keytype the type of the key, either "char" or "ansi" (from `system.readansi`) | ||
| 288 | -- @treturn string status the status of the key handling, either "ok", "exit_key" or an error message | ||
| 289 | function handle_key(self, key, keytype) | ||
| 290 | if self.exit_keys[key] then | ||
| 291 | -- registered exit key | ||
| 292 | return "exit_key" | ||
| 293 | end | ||
| 294 | |||
| 295 | local handler = key_handlers[key_names[key] or true ] | ||
| 296 | if handler then | ||
| 297 | handler(self) | ||
| 298 | return "ok" | ||
| 299 | end | ||
| 300 | |||
| 301 | if keytype == "ansi" then | ||
| 302 | -- we got an ansi sequence, but dunno how to handle it, ignore | ||
| 303 | -- print("unhandled ansi: ", key:sub(2,-1), string.byte(key, 1, -1)) | ||
| 304 | bell() | ||
| 305 | return "ok" | ||
| 306 | end | ||
| 307 | |||
| 308 | -- just a single key | ||
| 309 | if key < " " then | ||
| 310 | -- control character | ||
| 311 | bell() | ||
| 312 | return "ok" | ||
| 313 | end | ||
| 314 | |||
| 315 | if self.value.chars >= self.max_length then | ||
| 316 | bell() | ||
| 317 | return "ok" | ||
| 318 | end | ||
| 319 | |||
| 320 | -- insert the key into the value | ||
| 321 | if sys.utf8cwidth(key) == 2 then | ||
| 322 | -- double width character, insert empty string after it | ||
| 323 | table.insert(self.value, self.position, "") | ||
| 324 | table.insert(self.value, self.position, key) | ||
| 325 | self.position = self.position + 2 | ||
| 326 | io.write(cursor_move_horiz(2)) | ||
| 327 | else | ||
| 328 | table.insert(self.value, self.position, key) | ||
| 329 | self.position = self.position + 1 | ||
| 330 | io.write(cursor_move_horiz(1)) | ||
| 331 | end | ||
| 332 | self.value.chars = self.value.chars + 1 | ||
| 333 | draw(self) | ||
| 334 | return "ok" | ||
| 335 | end | ||
| 336 | end | ||
| 337 | |||
| 338 | |||
| 339 | |||
| 340 | --- Get_size returns the maximum size of the input box (prompt + input). | ||
| 341 | -- The size is in rows and columns. Columns is determined by | ||
| 342 | -- the prompt and the `max_length * 2` (characters can be double-width). | ||
| 343 | -- @treturn number the number of rows (always 1) | ||
| 344 | -- @treturn number the number of columns | ||
| 345 | function readline:get_size() | ||
| 346 | return 1, #self.prompt + self.max_length * 2 | ||
| 347 | end | ||
| 348 | |||
| 349 | |||
| 350 | |||
| 351 | --- Get coordinates of the cursor in the input box (prompt + input). | ||
| 352 | -- The coordinates are 1-based. They are returned as row and column, within the | ||
| 353 | -- size as reported by `get_size`. | ||
| 354 | -- @treturn number the row of the cursor (always 1) | ||
| 355 | -- @treturn number the column of the cursor | ||
| 356 | function readline:get_cursor() | ||
| 357 | return 1, #self.prompt + self.position | ||
| 358 | end | ||
| 359 | |||
| 360 | |||
| 361 | |||
| 362 | --- Set the coordinates of the cursor in the input box (prompt + input). | ||
| 363 | -- The coordinates are 1-based. They are expected to be within the | ||
| 364 | -- size as reported by `get_size`, and beyond the prompt. | ||
| 365 | -- If the position is invalid, it will be corrected. | ||
| 366 | -- Use the results to check if the position was adjusted. | ||
| 367 | -- @tparam number row the row of the cursor (always 1) | ||
| 368 | -- @tparam number col the column of the cursor | ||
| 369 | -- @return results of get_cursor | ||
| 370 | function readline:set_cursor(row, col) | ||
| 371 | local l_prompt = #self.prompt | ||
| 372 | local l_value = #self.value | ||
| 373 | |||
| 374 | if col < l_prompt + 1 then | ||
| 375 | col = l_prompt + 1 | ||
| 376 | elseif col > l_prompt + l_value + 1 then | ||
| 377 | col = l_prompt + l_value + 1 | ||
| 378 | end | ||
| 379 | |||
| 380 | while self.value[col - l_prompt] == "" do | ||
| 381 | col = col - 1 -- on an empty string, so move back to start of double-width char | ||
| 382 | end | ||
| 383 | |||
| 384 | local new_pos = col - l_prompt | ||
| 385 | |||
| 386 | cursor_move_horiz(self.position - new_pos) | ||
| 387 | io.flush() | ||
| 388 | |||
| 389 | self.position = new_pos | ||
| 390 | return self:get_cursor() | ||
| 391 | end | ||
| 392 | |||
| 393 | |||
| 394 | |||
| 395 | --- Read a line of input from the user. | ||
| 396 | -- It will first print the `prompt` and then wait for input. Ensure the cursor | ||
| 397 | -- is at the correct position before calling this function. This function will | ||
| 398 | -- do all cursor movements in a relative way. | ||
| 399 | -- Can be called again after an exit-key or timeout has occurred. Just make sure | ||
| 400 | -- the cursor is at the same position where is was when it returned the last time. | ||
| 401 | -- Alternatively the cursor can be set to the position of the prompt (the position | ||
| 402 | -- the cursor was in before the first call), and the parameter `redraw` can be set | ||
| 403 | -- to `true`. | ||
| 404 | -- @tparam[opt=math.huge] number timeout the maximum time to wait for input in seconds | ||
| 405 | -- @tparam[opt=false] boolean redraw if `true` the prompt will be redrawn (cursor must be at prompt position!) | ||
| 406 | -- @treturn[1] string the input string as entered the user | ||
| 407 | -- @treturn[1] string the exit-key used to exit the readline (see `new`) | ||
| 408 | -- @treturn[2] nil when input is incomplete | ||
| 409 | -- @treturn[2] string error message, the reason why the input is incomplete, `"timeout"`, or an error reading a key | ||
| 410 | function readline:__call(timeout, redraw) | ||
| 411 | draw(self, redraw) | ||
| 412 | timeout = timeout or math.huge | ||
| 413 | local timeout_end = sys.gettime() + timeout | ||
| 414 | |||
| 415 | while true do | ||
| 416 | local key, keytype = sys.readansi(timeout_end - sys.gettime()) | ||
| 417 | if not key then | ||
| 418 | -- error or timeout | ||
| 419 | return nil, keytype | ||
| 420 | end | ||
| 421 | |||
| 422 | local status = handle_key(self, key, keytype) | ||
| 423 | if status == "exit_key" then | ||
| 424 | return tostring(self.value), key | ||
| 425 | |||
| 426 | elseif status ~= "ok" then | ||
| 427 | error("unknown status received: " .. tostring(status)) | ||
| 428 | end | ||
| 429 | end | ||
| 430 | end | ||
| 431 | |||
| 432 | |||
| 433 | |||
| 434 | -- return readline -- normally we'd return here, but for the example we continue | ||
| 435 | |||
| 436 | |||
| 437 | |||
| 438 | |||
| 439 | local backup = sys.termbackup() | ||
| 440 | |||
| 441 | -- setup Windows console to handle ANSI processing | ||
| 442 | sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
| 443 | sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT) | ||
| 444 | -- set output to UTF-8 | ||
| 445 | sys.setconsoleoutputcp(sys.CODEPAGE_UTF8) | ||
| 446 | |||
| 447 | -- setup Posix terminal to disable canonical mode and echo | ||
| 448 | sys.tcsetattr(io.stdin, sys.TCSANOW, { | ||
| 449 | lflag = sys.tcgetattr(io.stdin).lflag - sys.L_ICANON - sys.L_ECHO, | ||
| 450 | }) | ||
| 451 | -- setup stdin to non-blocking mode | ||
| 452 | sys.setnonblock(io.stdin, true) | ||
| 453 | |||
| 454 | |||
| 455 | local rl = readline.new{ | ||
| 456 | prompt = "Enter something: ", | ||
| 457 | max_length = 60, | ||
| 458 | value = "Hello, 你-好 World 🚀!", | ||
| 459 | -- position = 2, | ||
| 460 | exit_keys = {key_sequences.enter, "\27", "\t", "\27[Z"}, -- enter, escape, tab, shift-tab | ||
| 461 | } | ||
| 462 | |||
| 463 | |||
| 464 | local result, key = rl() | ||
| 465 | print("") -- newline after input, to move cursor down from the input line | ||
| 466 | print("Result (string): '" .. result .. "'") | ||
| 467 | print("Result (bytes):", result:byte(1,-1)) | ||
| 468 | print("Exit-Key (bytes):", key:byte(1,-1)) | ||
| 469 | |||
| 470 | |||
| 471 | -- Clean up afterwards | ||
| 472 | sys.termrestore(backup) | ||
diff --git a/examples/spinner.lua b/examples/spinner.lua new file mode 100644 index 0000000..e518e60 --- /dev/null +++ b/examples/spinner.lua | |||
| @@ -0,0 +1,64 @@ | |||
| 1 | local sys = require("system") | ||
| 2 | |||
| 3 | print [[ | ||
| 4 | |||
| 5 | An example to display a spinner, whilst a long running task executes. | ||
| 6 | |||
| 7 | ]] | ||
| 8 | |||
| 9 | |||
| 10 | -- start make backup, to auto-restore on exit | ||
| 11 | sys.autotermrestore() | ||
| 12 | -- configure console | ||
| 13 | sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT) | ||
| 14 | local of = sys.tcgetattr(io.stdin) | ||
| 15 | sys.tcsetattr(io.stdin, sys.TCSANOW, { lflag = of.lflag - sys.L_ICANON - sys.L_ECHO }) | ||
| 16 | sys.setnonblock(io.stdin, true) | ||
| 17 | |||
| 18 | |||
| 19 | |||
| 20 | local function hideCursor() | ||
| 21 | io.write("\27[?25l") | ||
| 22 | io.flush() | ||
| 23 | end | ||
| 24 | |||
| 25 | local function showCursor() | ||
| 26 | io.write("\27[?25h") | ||
| 27 | io.flush() | ||
| 28 | end | ||
| 29 | |||
| 30 | local function left(n) | ||
| 31 | io.write("\27[",n or 1,"D") | ||
| 32 | io.flush() | ||
| 33 | end | ||
| 34 | |||
| 35 | |||
| 36 | |||
| 37 | local spinner do | ||
| 38 | local spin = [[|/-\]] | ||
| 39 | local i = 1 | ||
| 40 | spinner = function() | ||
| 41 | hideCursor() | ||
| 42 | io.write(spin:sub(i, i)) | ||
| 43 | left() | ||
| 44 | i = i + 1 | ||
| 45 | if i > #spin then i = 1 end | ||
| 46 | |||
| 47 | if sys.readkey(0) ~= nil then | ||
| 48 | while sys.readkey(0) ~= nil do end -- consume keys pressed | ||
| 49 | io.write(" "); | ||
| 50 | left() | ||
| 51 | showCursor() | ||
| 52 | return true | ||
| 53 | else | ||
| 54 | return false | ||
| 55 | end | ||
| 56 | end | ||
| 57 | end | ||
| 58 | |||
| 59 | io.stdout:write("press any key to stop the spinner... ") | ||
| 60 | while not spinner() do | ||
| 61 | sys.sleep(0.1) | ||
| 62 | end | ||
| 63 | |||
| 64 | print("Done!") | ||
diff --git a/examples/spiral_snake.lua b/examples/spiral_snake.lua new file mode 100644 index 0000000..84a2040 --- /dev/null +++ b/examples/spiral_snake.lua | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | local sys = require "system" | ||
| 2 | |||
| 3 | print [[ | ||
| 4 | |||
| 5 | This example will draw a snake like spiral on the screen. Showing ANSI escape | ||
| 6 | codes for moving the cursor around. | ||
| 7 | |||
| 8 | ]] | ||
| 9 | |||
| 10 | -- backup term settings with auto-restore on exit | ||
| 11 | sys.autotermrestore() | ||
| 12 | |||
| 13 | -- setup Windows console to handle ANSI processing | ||
| 14 | sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
| 15 | |||
| 16 | -- start drawing the spiral. | ||
| 17 | -- start from current pos, then right, then up, then left, then down, and again. | ||
| 18 | local x, y = 1, 1 -- current position | ||
| 19 | local dx, dy = 1, 0 -- direction after each step | ||
| 20 | local wx, wy = 30, 30 -- width and height of the room | ||
| 21 | local mx, my = 1, 1 -- margin | ||
| 22 | |||
| 23 | -- commands to move the cursor | ||
| 24 | local move_left = "\27[1D" | ||
| 25 | local move_right = "\27[1C" | ||
| 26 | local move_up = "\27[1A" | ||
| 27 | local move_down = "\27[1B" | ||
| 28 | |||
| 29 | -- create room: 30 empty lines | ||
| 30 | print(("\n"):rep(wy)) | ||
| 31 | local move = move_right | ||
| 32 | |||
| 33 | while wx > 0 and wy > 0 do | ||
| 34 | sys.sleep(0.01) -- slow down the drawing a little | ||
| 35 | io.write("*" .. move_left .. move ) | ||
| 36 | io.flush() | ||
| 37 | x = x + dx | ||
| 38 | y = y + dy | ||
| 39 | |||
| 40 | if x > wx and move == move_right then | ||
| 41 | -- end of move right | ||
| 42 | dx = 0 | ||
| 43 | dy = 1 | ||
| 44 | move = move_up | ||
| 45 | wy = wy - 1 | ||
| 46 | my = my + 1 | ||
| 47 | elseif y > wy and move == move_up then | ||
| 48 | -- end of move up | ||
| 49 | dx = -1 | ||
| 50 | dy = 0 | ||
| 51 | move = move_left | ||
| 52 | wx = wx - 1 | ||
| 53 | mx = mx + 1 | ||
| 54 | elseif x < mx and move == move_left then | ||
| 55 | -- end of move left | ||
| 56 | dx = 0 | ||
| 57 | dy = -1 | ||
| 58 | move = move_down | ||
| 59 | wy = wy - 1 | ||
| 60 | my = my + 1 | ||
| 61 | elseif y < my and move == move_down then | ||
| 62 | -- end of move down | ||
| 63 | dx = 1 | ||
| 64 | dy = 0 | ||
| 65 | move = move_right | ||
| 66 | wx = wx - 1 | ||
| 67 | mx = mx + 1 | ||
| 68 | end | ||
| 69 | end | ||
| 70 | |||
| 71 | io.write(move_down:rep(15)) | ||
| 72 | print("\nDone!") | ||
diff --git a/examples/terminalsize.lua b/examples/terminalsize.lua new file mode 100644 index 0000000..105a415 --- /dev/null +++ b/examples/terminalsize.lua | |||
| @@ -0,0 +1,37 @@ | |||
| 1 | local sys = require("system") | ||
| 2 | |||
| 3 | sys.autotermrestore() -- set up auto restore of terminal settings on exit | ||
| 4 | |||
| 5 | -- setup Windows console to handle ANSI processing | ||
| 6 | sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
| 7 | sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT) | ||
| 8 | |||
| 9 | -- setup Posix to disable canonical mode and echo | ||
| 10 | local of_attr = sys.tcgetattr(io.stdin) | ||
| 11 | sys.setnonblock(io.stdin, true) | ||
| 12 | sys.tcsetattr(io.stdin, sys.TCSANOW, { | ||
| 13 | lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, -- disable canonical mode and echo | ||
| 14 | }) | ||
| 15 | |||
| 16 | |||
| 17 | -- generate string to move cursor horizontally | ||
| 18 | -- positive goes right, negative goes left | ||
| 19 | local function cursor_move_horiz(n) | ||
| 20 | if n == 0 then | ||
| 21 | return "" | ||
| 22 | end | ||
| 23 | return "\27[" .. (n > 0 and n or -n) .. (n > 0 and "C" or "D") | ||
| 24 | end | ||
| 25 | |||
| 26 | |||
| 27 | local rows, cols | ||
| 28 | print("Change the terminal window size, press any key to exit") | ||
| 29 | while not sys.readansi(0.2) do -- use readansi to not leave stray bytes in the input buffer | ||
| 30 | local nrows, ncols = sys.termsize() | ||
| 31 | if rows ~= nrows or cols ~= ncols then | ||
| 32 | rows, cols = nrows, ncols | ||
| 33 | local text = "Terminal size: " .. rows .. "x" .. cols .. " " | ||
| 34 | io.write(text .. cursor_move_horiz(-#text)) | ||
| 35 | io.flush() | ||
| 36 | end | ||
| 37 | end | ||
