aboutsummaryrefslogtreecommitdiff
path: root/system
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2024-06-20 22:43:06 +0200
committerGitHub <noreply@github.com>2024-06-20 22:43:06 +0200
commitc1a64c1b75f97cef97965b3bd9a941564a6270bd (patch)
treeb9a92dff6462abd5859c3c76f19748fad5d6c025 /system
parent47c24eed0191f8f72646be63dee94ac2b35eb062 (diff)
parentb87e6d6d762ee823e81dd7a8984f330eb4018fd8 (diff)
downloadluasystem-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.lua378
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
1local system = require 'system.core' 7local 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.
13system.CODEPAGE_UTF8 = 65001
14
15do
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
73end
74
75
76do -- 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
124end
125
126
127
128do
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
149end
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)
160function 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
188end
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)
199function 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
226end
227
228
229
230do
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
261end
262
263
264
265do
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
376end
377
378
379
2return system 380return system