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 /system | |
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 'system')
-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 |