diff options
Diffstat (limited to '')
| -rw-r--r-- | system/init.lua | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/system/init.lua b/system/init.lua index 77e0c3b..e99d0d4 100644 --- a/system/init.lua +++ b/system/init.lua | |||
| @@ -1,2 +1,380 @@ | |||
| 1 | --- Lua System Library. | ||
| 2 | -- @module system | ||
| 3 | |||
| 4 | --- Terminal | ||
| 5 | -- @section terminal | ||
| 6 | |||
| 1 | local system = require 'system.core' | 7 | local system = require 'system.core' |
| 8 | |||
| 9 | |||
| 10 | --- UTF8 codepage. | ||
| 11 | -- To be used with `system.setconsoleoutputcp` and `system.setconsolecp`. | ||
| 12 | -- @field CODEPAGE_UTF8 The Windows CodePage for UTF8. | ||
| 13 | system.CODEPAGE_UTF8 = 65001 | ||
| 14 | |||
| 15 | do | ||
| 16 | local backup_mt = {} | ||
| 17 | |||
| 18 | --- Returns a backup of terminal settings for stdin/out/err. | ||
| 19 | -- Handles terminal/console flags, Windows codepage, and non-block flags on the streams. | ||
| 20 | -- Backs up terminal/console flags only if a stream is a tty. | ||
| 21 | -- @return table with backup of terminal settings | ||
| 22 | function system.termbackup() | ||
| 23 | local backup = setmetatable({}, backup_mt) | ||
| 24 | |||
| 25 | if system.isatty(io.stdin) then | ||
| 26 | backup.console_in = system.getconsoleflags(io.stdin) | ||
| 27 | backup.term_in = system.tcgetattr(io.stdin) | ||
| 28 | end | ||
| 29 | if system.isatty(io.stdout) then | ||
| 30 | backup.console_out = system.getconsoleflags(io.stdout) | ||
| 31 | backup.term_out = system.tcgetattr(io.stdout) | ||
| 32 | end | ||
| 33 | if system.isatty(io.stderr) then | ||
| 34 | backup.console_err = system.getconsoleflags(io.stderr) | ||
| 35 | backup.term_err = system.tcgetattr(io.stderr) | ||
| 36 | end | ||
| 37 | |||
| 38 | backup.block_in = system.getnonblock(io.stdin) | ||
| 39 | backup.block_out = system.getnonblock(io.stdout) | ||
| 40 | backup.block_err = system.getnonblock(io.stderr) | ||
| 41 | |||
| 42 | backup.consoleoutcodepage = system.getconsoleoutputcp() | ||
| 43 | backup.consolecp = system.getconsolecp() | ||
| 44 | |||
| 45 | return backup | ||
| 46 | end | ||
| 47 | |||
| 48 | |||
| 49 | |||
| 50 | --- Restores terminal settings from a backup | ||
| 51 | -- @tparam table backup the backup of terminal settings, see `termbackup`. | ||
| 52 | -- @treturn boolean true | ||
| 53 | function system.termrestore(backup) | ||
| 54 | if getmetatable(backup) ~= backup_mt then | ||
| 55 | error("arg #1 to termrestore, expected backup table, got " .. type(backup), 2) | ||
| 56 | end | ||
| 57 | |||
| 58 | if backup.console_in then system.setconsoleflags(io.stdin, backup.console_in) end | ||
| 59 | if backup.term_in then system.tcsetattr(io.stdin, system.TCSANOW, backup.term_in) end | ||
| 60 | if backup.console_out then system.setconsoleflags(io.stdout, backup.console_out) end | ||
| 61 | if backup.term_out then system.tcsetattr(io.stdout, system.TCSANOW, backup.term_out) end | ||
| 62 | if backup.console_err then system.setconsoleflags(io.stderr, backup.console_err) end | ||
| 63 | if backup.term_err then system.tcsetattr(io.stderr, system.TCSANOW, backup.term_err) end | ||
| 64 | |||
| 65 | if backup.block_in ~= nil then system.setnonblock(io.stdin, backup.block_in) end | ||
| 66 | if backup.block_out ~= nil then system.setnonblock(io.stdout, backup.block_out) end | ||
| 67 | if backup.block_err ~= nil then system.setnonblock(io.stderr, backup.block_err) end | ||
| 68 | |||
| 69 | if backup.consoleoutcodepage then system.setconsoleoutputcp(backup.consoleoutcodepage) end | ||
| 70 | if backup.consolecp then system.setconsolecp(backup.consolecp) end | ||
| 71 | return true | ||
| 72 | end | ||
| 73 | end | ||
| 74 | |||
| 75 | |||
| 76 | do -- autotermrestore | ||
| 77 | local global_backup -- global backup for terminal settings | ||
| 78 | |||
| 79 | |||
| 80 | local add_gc_method do | ||
| 81 | -- __gc meta-method is not available in all Lua versions | ||
| 82 | local has_gc = not newproxy or false -- `__gc` was added when `newproxy` was removed | ||
| 83 | |||
| 84 | if has_gc then | ||
| 85 | -- use default GC mechanism since it is available | ||
| 86 | function add_gc_method(t, f) | ||
| 87 | setmetatable(t, { __gc = f }) | ||
| 88 | end | ||
| 89 | else | ||
| 90 | -- create workaround using a proxy userdata, typical for Lua 5.1 | ||
| 91 | function add_gc_method(t, f) | ||
| 92 | local proxy = newproxy(true) | ||
| 93 | getmetatable(proxy).__gc = function() | ||
| 94 | t["__gc_proxy"] = nil | ||
| 95 | f(t) | ||
| 96 | end | ||
| 97 | t["__gc_proxy"] = proxy | ||
| 98 | end | ||
| 99 | end | ||
| 100 | end | ||
| 101 | |||
| 102 | |||
| 103 | --- Backs up terminal settings and restores them on application exit. | ||
| 104 | -- Calls `termbackup` to back up terminal settings and sets up a GC method to | ||
| 105 | -- automatically restore them on application exit (also works on Lua 5.1). | ||
| 106 | -- @treturn[1] boolean true | ||
| 107 | -- @treturn[2] nil if the backup was already created | ||
| 108 | -- @treturn[2] string error message | ||
| 109 | function system.autotermrestore() | ||
| 110 | if global_backup then | ||
| 111 | return nil, "global terminal backup was already set up" | ||
| 112 | end | ||
| 113 | global_backup = system.termbackup() | ||
| 114 | add_gc_method(global_backup, function(self) pcall(system.termrestore, self) end) | ||
| 115 | return true | ||
| 116 | end | ||
| 117 | |||
| 118 | -- export a reset function only upon testing | ||
| 119 | if _G._TEST then | ||
| 120 | function system._reset_global_backup() | ||
| 121 | global_backup = nil | ||
| 122 | end | ||
| 123 | end | ||
| 124 | end | ||
| 125 | |||
| 126 | |||
| 127 | |||
| 128 | do | ||
| 129 | local oldunpack = unpack or table.unpack | ||
| 130 | local pack = function(...) return { n = select("#", ...), ... } end | ||
| 131 | local unpack = function(t) return oldunpack(t, 1, t.n) end | ||
| 132 | |||
| 133 | --- Wraps a function to automatically restore terminal settings upon returning. | ||
| 134 | -- Calls `termbackup` before calling the function and `termrestore` after. | ||
| 135 | -- @tparam function f function to wrap | ||
| 136 | -- @treturn function wrapped function | ||
| 137 | function system.termwrap(f) | ||
| 138 | if type(f) ~= "function" then | ||
| 139 | error("arg #1 to wrap, expected function, got " .. type(f), 2) | ||
| 140 | end | ||
| 141 | |||
| 142 | return function(...) | ||
| 143 | local bu = system.termbackup() | ||
| 144 | local results = pack(f(...)) | ||
| 145 | system.termrestore(bu) | ||
| 146 | return unpack(results) | ||
| 147 | end | ||
| 148 | end | ||
| 149 | end | ||
| 150 | |||
| 151 | |||
| 152 | |||
| 153 | --- Debug function for console flags (Windows). | ||
| 154 | -- Pretty prints the current flags set for the handle. | ||
| 155 | -- @param fh file handle (`io.stdin`, `io.stdout`, `io.stderr`) | ||
| 156 | -- @usage -- Print the flags for stdin/out/err | ||
| 157 | -- system.listconsoleflags(io.stdin) | ||
| 158 | -- system.listconsoleflags(io.stdout) | ||
| 159 | -- system.listconsoleflags(io.stderr) | ||
| 160 | function system.listconsoleflags(fh) | ||
| 161 | local flagtype | ||
| 162 | if fh == io.stdin then | ||
| 163 | print "------ STDIN FLAGS WINDOWS ------" | ||
| 164 | flagtype = "CIF_" | ||
| 165 | elseif fh == io.stdout then | ||
| 166 | print "------ STDOUT FLAGS WINDOWS ------" | ||
| 167 | flagtype = "COF_" | ||
| 168 | elseif fh == io.stderr then | ||
| 169 | print "------ STDERR FLAGS WINDOWS ------" | ||
| 170 | flagtype = "COF_" | ||
| 171 | end | ||
| 172 | |||
| 173 | local flags = assert(system.getconsoleflags(fh)) | ||
| 174 | local out = {} | ||
| 175 | for k,v in pairs(system) do | ||
| 176 | if type(k) == "string" and k:sub(1,4) == flagtype then | ||
| 177 | if flags:has_all_of(v) then | ||
| 178 | out[#out+1] = string.format("%10d [x] %s",v:value(),k) | ||
| 179 | else | ||
| 180 | out[#out+1] = string.format("%10d [ ] %s",v:value(),k) | ||
| 181 | end | ||
| 182 | end | ||
| 183 | end | ||
| 184 | table.sort(out) | ||
| 185 | for k,v in pairs(out) do | ||
| 186 | print(v) | ||
| 187 | end | ||
| 188 | end | ||
| 189 | |||
| 190 | |||
| 191 | |||
| 192 | --- Debug function for terminal flags (Posix). | ||
| 193 | -- Pretty prints the current flags set for the handle. | ||
| 194 | -- @param fh file handle (`io.stdin`, `io.stdout`, `io.stderr`) | ||
| 195 | -- @usage -- Print the flags for stdin/out/err | ||
| 196 | -- system.listconsoleflags(io.stdin) | ||
| 197 | -- system.listconsoleflags(io.stdout) | ||
| 198 | -- system.listconsoleflags(io.stderr) | ||
| 199 | function system.listtermflags(fh) | ||
| 200 | if fh == io.stdin then | ||
| 201 | print "------ STDIN FLAGS POSIX ------" | ||
| 202 | elseif fh == io.stdout then | ||
| 203 | print "------ STDOUT FLAGS POSIX ------" | ||
| 204 | elseif fh == io.stderr then | ||
| 205 | print "------ STDERR FLAGS POSIX ------" | ||
| 206 | end | ||
| 207 | |||
| 208 | local flags = assert(system.tcgetattr(fh)) | ||
| 209 | for _, flagtype in ipairs { "iflag", "oflag", "lflag" } do | ||
| 210 | local prefix = flagtype:sub(1,1):upper() .. "_" -- I_, O_, or L_, the constant prefixes | ||
| 211 | local out = {} | ||
| 212 | for k,v in pairs(system) do | ||
| 213 | if type(k) == "string" and k:sub(1,2) == prefix then | ||
| 214 | if flags[flagtype]:has_all_of(v) then | ||
| 215 | out[#out+1] = string.format("%10d [x] %s",v:value(),k) | ||
| 216 | else | ||
| 217 | out[#out+1] = string.format("%10d [ ] %s",v:value(),k) | ||
| 218 | end | ||
| 219 | end | ||
| 220 | end | ||
| 221 | table.sort(out) | ||
| 222 | for k,v in pairs(out) do | ||
| 223 | print(v) | ||
| 224 | end | ||
| 225 | end | ||
| 226 | end | ||
| 227 | |||
| 228 | |||
| 229 | |||
| 230 | do | ||
| 231 | --- Reads a single byte from the console, with a timeout. | ||
| 232 | -- This function uses `system.sleep` to wait until either a byte is available or the timeout is reached. | ||
| 233 | -- The sleep period is exponentially backing off, starting at 0.0125 seconds, with a maximum of 0.2 seconds. | ||
| 234 | -- It returns immediately if a byte is available or if `timeout` is less than or equal to `0`. | ||
| 235 | -- | ||
| 236 | -- Using `system.readansi` is preferred over this function. Since this function can leave stray/invalid | ||
| 237 | -- byte-sequences in the input buffer, while `system.readansi` reads full ANSI and UTF8 sequences. | ||
| 238 | -- @tparam number timeout the timeout in seconds. | ||
| 239 | -- @treturn[1] byte the byte value that was read. | ||
| 240 | -- @treturn[2] nil if no key was read | ||
| 241 | -- @treturn[2] string error message; `"timeout"` if the timeout was reached. | ||
| 242 | function system.readkey(timeout) | ||
| 243 | if type(timeout) ~= "number" then | ||
| 244 | error("arg #1 to readkey, expected timeout in seconds, got " .. type(timeout), 2) | ||
| 245 | end | ||
| 246 | |||
| 247 | local interval = 0.0125 | ||
| 248 | local key = system._readkey() | ||
| 249 | while key == nil and timeout > 0 do | ||
| 250 | system.sleep(math.min(interval, timeout)) | ||
| 251 | timeout = timeout - interval | ||
| 252 | interval = math.min(0.2, interval * 2) | ||
| 253 | key = system._readkey() | ||
| 254 | end | ||
| 255 | |||
| 256 | if key then | ||
| 257 | return key | ||
| 258 | end | ||
| 259 | return nil, "timeout" | ||
| 260 | end | ||
| 261 | end | ||
| 262 | |||
| 263 | |||
| 264 | |||
| 265 | do | ||
| 266 | local left_over_key | ||
| 267 | local sequence -- table to store the sequence in progress | ||
| 268 | local utf8_length -- length of utf8 sequence currently being processed | ||
| 269 | local unpack = unpack or table.unpack | ||
| 270 | |||
| 271 | --- Reads a single key, if it is the start of ansi escape sequence then it reads | ||
| 272 | -- the full sequence. The key can be a multi-byte string in case of multibyte UTF-8 character. | ||
| 273 | -- This function uses `system.readkey`, and hence `system.sleep` to wait until either a key is | ||
| 274 | -- available or the timeout is reached. | ||
| 275 | -- It returns immediately if a key is available or if `timeout` is less than or equal to `0`. | ||
| 276 | -- In case of an ANSI sequence, it will return the full sequence as a string. | ||
| 277 | -- @tparam number timeout the timeout in seconds. | ||
| 278 | -- @treturn[1] string the character that was received (can be multi-byte), or a complete ANSI sequence | ||
| 279 | -- @treturn[1] string the type of input: `"char"` for a single key, `"ansi"` for an ANSI sequence | ||
| 280 | -- @treturn[2] nil in case of an error | ||
| 281 | -- @treturn[2] string error message; `"timeout"` if the timeout was reached. | ||
| 282 | -- @treturn[2] string partial result in case of an error while reading a sequence, the sequence so far. | ||
| 283 | function system.readansi(timeout) | ||
| 284 | if type(timeout) ~= "number" then | ||
| 285 | error("arg #1 to readansi, expected timeout in seconds, got " .. type(timeout), 2) | ||
| 286 | end | ||
| 287 | |||
| 288 | local key | ||
| 289 | |||
| 290 | if not sequence then | ||
| 291 | -- no sequence in progress, read a key | ||
| 292 | |||
| 293 | if left_over_key then | ||
| 294 | -- we still have a cached key from the last call | ||
| 295 | key = left_over_key | ||
| 296 | left_over_key = nil | ||
| 297 | else | ||
| 298 | -- read a new key | ||
| 299 | local err | ||
| 300 | key, err = system.readkey(timeout) | ||
| 301 | if key == nil then -- timeout or error | ||
| 302 | return nil, err | ||
| 303 | end | ||
| 304 | end | ||
| 305 | |||
| 306 | if key == 27 then | ||
| 307 | -- looks like an ansi escape sequence, immediately read next char | ||
| 308 | -- as an heuristic against manually typing escape sequences | ||
| 309 | local key2 = system.readkey(0) | ||
| 310 | if key2 ~= 91 and key2 ~= 79 then -- we expect either "[" or "O" for an ANSI sequence | ||
| 311 | -- not the expected [ or O character, so we return the key as is | ||
| 312 | -- and store the extra key read for the next call | ||
| 313 | left_over_key = key2 | ||
| 314 | return string.char(key), "char" | ||
| 315 | end | ||
| 316 | |||
| 317 | -- escape sequence detected | ||
| 318 | sequence = { key, key2 } | ||
| 319 | else | ||
| 320 | -- check UTF8 length | ||
| 321 | utf8_length = key < 128 and 1 or key < 224 and 2 or key < 240 and 3 or key < 248 and 4 | ||
| 322 | if utf8_length == 1 then | ||
| 323 | -- single byte character | ||
| 324 | utf8_length = nil | ||
| 325 | return string.char(key), "char" | ||
| 326 | else | ||
| 327 | -- UTF8 sequence detected | ||
| 328 | sequence = { key } | ||
| 329 | end | ||
| 330 | end | ||
| 331 | end | ||
| 332 | |||
| 333 | local err | ||
| 334 | if utf8_length then | ||
| 335 | -- read remainder of UTF8 sequence | ||
| 336 | local timeout_end = system.gettime() + timeout | ||
| 337 | while true do | ||
| 338 | key, err = system.readkey(timeout_end - system.gettime()) | ||
| 339 | if err then | ||
| 340 | break | ||
| 341 | end | ||
| 342 | table.insert(sequence, key) | ||
| 343 | |||
| 344 | if #sequence == utf8_length then | ||
| 345 | -- end of sequence, return the full sequence | ||
| 346 | local result = string.char(unpack(sequence)) | ||
| 347 | sequence = nil | ||
| 348 | utf8_length = nil | ||
| 349 | return result, "char" | ||
| 350 | end | ||
| 351 | end | ||
| 352 | |||
| 353 | else | ||
| 354 | -- read remainder of ANSI sequence | ||
| 355 | local timeout_end = system.gettime() + timeout | ||
| 356 | while true do | ||
| 357 | key, err = system.readkey(timeout_end - system.gettime()) | ||
| 358 | if err then | ||
| 359 | break | ||
| 360 | end | ||
| 361 | table.insert(sequence, key) | ||
| 362 | |||
| 363 | if (key >= 65 and key <= 90) or (key >= 97 and key <= 126) then | ||
| 364 | -- end of sequence, return the full sequence | ||
| 365 | local result = string.char(unpack(sequence)) | ||
| 366 | sequence = nil | ||
| 367 | return result, "ansi" | ||
| 368 | end | ||
| 369 | end | ||
| 370 | end | ||
| 371 | |||
| 372 | -- error, or timeout reached, return the sequence so far | ||
| 373 | local partial = string.char(unpack(sequence)) | ||
| 374 | return nil, err, partial | ||
| 375 | end | ||
| 376 | end | ||
| 377 | |||
| 378 | |||
| 379 | |||
| 2 | return system | 380 | return system |
