aboutsummaryrefslogtreecommitdiff
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
parent47c24eed0191f8f72646be63dee94ac2b35eb062 (diff)
parentb87e6d6d762ee823e81dd7a8984f330eb4018fd8 (diff)
downloadluasystem-c1a64c1b75f97cef97965b3bd9a941564a6270bd.tar.gz
luasystem-c1a64c1b75f97cef97965b3bd9a941564a6270bd.tar.bz2
luasystem-c1a64c1b75f97cef97965b3bd9a941564a6270bd.zip
Merge pull request #21 from lunarmodules/terminal
-rw-r--r--Makefile10
-rw-r--r--config.ld3
-rw-r--r--doc_topics/03-terminal.md124
-rw-r--r--examples/compat.lua40
-rw-r--r--examples/flag_debugging.lua7
-rw-r--r--examples/password_input.lua59
-rw-r--r--examples/read.lua70
-rw-r--r--examples/readline.lua472
-rw-r--r--examples/spinner.lua64
-rw-r--r--examples/spiral_snake.lua72
-rw-r--r--examples/terminalsize.lua37
-rw-r--r--luasystem-scm-0.rockspec2
-rw-r--r--spec/04-term_spec.lua808
-rw-r--r--spec/05-bitflags_spec.lua114
-rw-r--r--src/bitflags.c270
-rw-r--r--src/bitflags.h30
-rw-r--r--src/compat.c16
-rw-r--r--src/compat.h12
-rw-r--r--src/core.c2
-rw-r--r--src/environment.c6
-rw-r--r--src/random.c7
-rw-r--r--src/term.c1133
-rw-r--r--src/time.c6
-rw-r--r--src/wcwidth.c285
-rw-r--r--src/wcwidth.h21
-rw-r--r--system/init.lua378
26 files changed, 4037 insertions, 11 deletions
diff --git a/Makefile b/Makefile
index 4f4d685..dd20ee9 100644
--- a/Makefile
+++ b/Makefile
@@ -45,3 +45,13 @@ install-all:
45 @cd src && $(MAKE) install LUA_VERSION=5.3 45 @cd src && $(MAKE) install LUA_VERSION=5.3
46 46
47.PHONY: test 47.PHONY: test
48test:
49 busted
50
51.PHONY: lint
52lint:
53 luacheck .
54
55.PHONY: docs
56docs:
57 ldoc .
diff --git a/config.ld b/config.ld
index c13936d..7d73609 100644
--- a/config.ld
+++ b/config.ld
@@ -8,9 +8,10 @@ style="./doc_topics/"
8 8
9file={'./src/', './system/'} 9file={'./src/', './system/'}
10topics={'./doc_topics/', './LICENSE.md', './CHANGELOG.md'} 10topics={'./doc_topics/', './LICENSE.md', './CHANGELOG.md'}
11-- examples = {'./examples'} 11examples = {'./examples'}
12 12
13dir='docs' 13dir='docs'
14sort=true 14sort=true
15sort_modules=true 15sort_modules=true
16all=false 16all=false
17merge=true
diff --git a/doc_topics/03-terminal.md b/doc_topics/03-terminal.md
new file mode 100644
index 0000000..9bad359
--- /dev/null
+++ b/doc_topics/03-terminal.md
@@ -0,0 +1,124 @@
1# 3. Terminal functionality
2
3Terminals are fundamentally different on Windows and Posix. So even though
4`luasystem` provides primitives to manipulate both the Windows and Posix terminals,
5the user will still have to write platform specific code.
6
7To mitigate this a little, all functions are available on all platforms. They just
8will be a no-op if invoked on another platform. This means that no platform specific
9branching is required (but still possible) in user code. The user must simply set
10up both platforms to make it work.
11
12## 3.1 Backup and Restore terminal settings
13
14Since there are a myriad of settings available;
15
16- `system.setconsoleflags` (Windows)
17- `system.setconsolecp` (Windows)
18- `system.setconsoleoutputcp` (Windows)
19- `system.setnonblock` (Posix)
20- `system.tcsetattr` (Posix)
21
22Some helper functions are available to backup and restore them all at once.
23See `termbackup`, `termrestore`, `autotermrestore` and `termwrap`.
24
25
26## 3.1 Terminal ANSI sequences
27
28Windows is catching up with this. In Windows 10 (since 2019), the Windows Terminal application (not to be
29mistaken for the `cmd` console application) supports ANSI sequences. However this
30might not be enabled by default.
31
32ANSI processing can be set up both on the input (key sequences, reading cursor position)
33as well as on the output (setting colors and cursor shapes).
34
35To enable it use `system.setconsoleflags` like this:
36
37 -- setup Windows console to handle ANSI processing on output
38 sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
39 sys.setconsoleflags(io.stderr, sys.getconsoleflags(io.stderr) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
40
41 -- setup Windows console to handle ANSI processing on input
42 sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT)
43
44
45## 3.2 UTF-8 in/output and display width
46
47### 3.2.1 UTF-8 in/output
48
49Where (most) Posix systems use UTF-8 by default, Windows internally uses UTF-16. More
50recent versions of Lua also have UTF-8 support. So `luasystem` also focusses on UTF-8.
51
52On Windows UTF-8 output can be enabled by setting the output codepage like this:
53
54 -- setup Windows output codepage to UTF-8
55 sys.setconsoleoutputcp(sys.CODEPAGE_UTF8)
56
57Terminal input is handled by the [`_getwchar()`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getchar-getwchar) function on Windows which returns
58UTF-16 surrogate pairs. `luasystem` will automatically convert those to UTF-8.
59So when using `readkey` or `readansi` to read keyboard input no additional changes
60are required.
61
62### 3.2.2 UTF-8 display width
63
64Typical western characters and symbols are single width characters and will use only
65a single column when displayed on a terminal. However many characters from other
66languages/cultures or emojis require 2 columns for display.
67
68Typically the `wcwidth` function is used on Posix to check the number of columns
69required for display. However since Windows doesn't provide this functionality a
70custom implementation is included based on [the work by Markus Kuhn](http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c).
71
722 functions are provided, `system.utf8cwidth` for a single character, and `system.utf8swidth` for
73a string. When writing terminal applications the display width is relevant to
74positioning the cursor properly. For an example see the [`examples/readline.lua`](../examples/readline.lua.html) file.
75
76
77## 3.3 reading keyboard input
78
79### 3.3.1 Non-blocking
80
81There are 2 functions for keyboard input (actually 3, if taking `system._readkey` into
82account): `readkey` and `readansi`.
83
84`readkey` is a low level function and should preferably not be used, it returns
85a byte at a time, and hence can leave stray/invalid byte sequences in the buffer if
86only the start of a UTF-8 or ANSI sequence is consumed.
87
88The preferred way is to use `readansi` which will parse and return entire characters in
89single or multiple bytes, or a full ANSI sequence.
90
91On Windows the input is read using [`_getwchar()`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getchar-getwchar) which bypasses the terminal and reads
92the input directly from the keyboard buffer. This means however that the character is
93also not being echoed to the terminal (independent of the echo settings used with
94`system.setconsoleflags`).
95
96On Posix the traditional file approach is used, which:
97
98- is blocking by default
99- echoes input to the terminal
100- requires enter to be pressed to pass the input (canonical mode)
101
102To use non-blocking input here's how to set it up:
103
104 -- setup Windows console to disable echo and line input (not required since _getwchar is used, just for consistency)
105 sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT)
106
107 -- setup Posix by disabling echo, canonical mode, and making non-blocking
108 local of_attr = sys.tcgetattr(io.stdin)
109 sys.tcsetattr(io.stdin, sys.TCSANOW, {
110 lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO,
111 })
112 sys.setnonblock(io.stdin, true)
113
114
115Both functions require a timeout to be provided which allows for proper asynchronous
116code to be written. Since the underlying sleep method used is `system.sleep`, just patching
117that function with a coroutine based yielding one should be all that is needed to make
118the result work with asynchroneous coroutine schedulers.
119
120### 3.3.2 Blocking input
121
122When using traditional input method like `io.stdin:read()` (which is blocking) the echo
123and newline properties should be set on Windows similar to Posix.
124For an example see [`examples/password_input.lua`](../examples/password_input.lua.html).
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
4local sys = require "system"
5
6
7
8if 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
29else
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
39end
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 @@
1local sys = require "system"
2
3-- Print the Windows Console flags for stdin
4sys.listconsoleflags(io.stdin)
5
6-- Print the Posix termios flags for stdin
7sys.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 @@
1local sys = require "system"
2
3print [[
4
5This example shows how to disable the "echo" of characters read to the console,
6useful 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(...)`
15local 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
37end
38
39
40
41-- Get username
42io.write("Username: ")
43local username = io.stdin:read("*l")
44
45-- Get the secret
46io.write("Password: ")
47local password = read_secret("*l")
48
49-- Get domainname
50io.write("Domain : ")
51local domain = io.stdin:read("*l")
52
53
54-- Print the results
55print("")
56print("Here's what we got:")
57print(" username: " .. username)
58print(" password: " .. password)
59print(" 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 @@
1local sys = require "system"
2
3print [[
4
5This example shows how to do a non-blocking read from the cli.
6
7]]
8
9-- setup Windows console to handle ANSI processing
10local of_in = sys.getconsoleflags(io.stdin)
11local of_out = sys.getconsoleflags(io.stdout)
12sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
13sys.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
16local of_attr = sys.tcgetattr(io.stdin)
17local of_block = sys.getnonblock(io.stdin)
18sys.setnonblock(io.stdin, true)
19sys.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
24local get_cursor_pos = "\27[6n"
25
26
27
28print("Press a key, or 'A' to get cursor position, 'ESC' to exit")
29while 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
61end
62
63
64
65-- Clean up afterwards
66sys.setnonblock(io.stdin, false)
67sys.setconsoleflags(io.stdout, of_out)
68sys.setconsoleflags(io.stdin, of_in)
69sys.tcsetattr(io.stdin, sys.TCSANOW, of_attr)
70sys.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
7local sys = require("system")
8
9
10-- Mapping of key-sequences to key-names
11local 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
23if sys.windows then
24 key_names["\13"] = "enter"
25else
26 key_names["\10"] = "enter"
27end
28
29
30-- Mapping of key-names to key-sequences
31local key_sequences = {}
32for k, v in pairs(key_names) do
33 key_sequences[v] = k
34end
35
36
37-- bell character
38local function bell()
39 io.write("\7")
40 io.flush()
41end
42
43
44-- generate string to move cursor horizontally
45-- positive goes right, negative goes left
46local 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")
51end
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
78local function utf8size(b)
79 return b < 128 and 1 or b < 224 and 2 or b < 240 and 3 or b < 248 and 4
80end
81
82
83
84local 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
115end
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
136local readline = {}
137readline.__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
148function 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
182end
183
184
185
186-- draw the prompt and the input value, and position the cursor.
187local 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()
203end
204
205
206local 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
336end
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
345function readline:get_size()
346 return 1, #self.prompt + self.max_length * 2
347end
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
356function readline:get_cursor()
357 return 1, #self.prompt + self.position
358end
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
370function 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()
391end
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
410function 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
430end
431
432
433
434-- return readline -- normally we'd return here, but for the example we continue
435
436
437
438
439local backup = sys.termbackup()
440
441-- setup Windows console to handle ANSI processing
442sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
443sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT)
444-- set output to UTF-8
445sys.setconsoleoutputcp(sys.CODEPAGE_UTF8)
446
447-- setup Posix terminal to disable canonical mode and echo
448sys.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
452sys.setnonblock(io.stdin, true)
453
454
455local 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
464local result, key = rl()
465print("") -- newline after input, to move cursor down from the input line
466print("Result (string): '" .. result .. "'")
467print("Result (bytes):", result:byte(1,-1))
468print("Exit-Key (bytes):", key:byte(1,-1))
469
470
471-- Clean up afterwards
472sys.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 @@
1local sys = require("system")
2
3print [[
4
5An example to display a spinner, whilst a long running task executes.
6
7]]
8
9
10-- start make backup, to auto-restore on exit
11sys.autotermrestore()
12-- configure console
13sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT)
14local of = sys.tcgetattr(io.stdin)
15sys.tcsetattr(io.stdin, sys.TCSANOW, { lflag = of.lflag - sys.L_ICANON - sys.L_ECHO })
16sys.setnonblock(io.stdin, true)
17
18
19
20local function hideCursor()
21 io.write("\27[?25l")
22 io.flush()
23end
24
25local function showCursor()
26 io.write("\27[?25h")
27 io.flush()
28end
29
30local function left(n)
31 io.write("\27[",n or 1,"D")
32 io.flush()
33end
34
35
36
37local 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
57end
58
59io.stdout:write("press any key to stop the spinner... ")
60while not spinner() do
61 sys.sleep(0.1)
62end
63
64print("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 @@
1local sys = require "system"
2
3print [[
4
5This example will draw a snake like spiral on the screen. Showing ANSI escape
6codes for moving the cursor around.
7
8]]
9
10-- backup term settings with auto-restore on exit
11sys.autotermrestore()
12
13-- setup Windows console to handle ANSI processing
14sys.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.
18local x, y = 1, 1 -- current position
19local dx, dy = 1, 0 -- direction after each step
20local wx, wy = 30, 30 -- width and height of the room
21local mx, my = 1, 1 -- margin
22
23-- commands to move the cursor
24local move_left = "\27[1D"
25local move_right = "\27[1C"
26local move_up = "\27[1A"
27local move_down = "\27[1B"
28
29-- create room: 30 empty lines
30print(("\n"):rep(wy))
31local move = move_right
32
33while 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
69end
70
71io.write(move_down:rep(15))
72print("\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 @@
1local sys = require("system")
2
3sys.autotermrestore() -- set up auto restore of terminal settings on exit
4
5-- setup Windows console to handle ANSI processing
6sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
7sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT)
8
9-- setup Posix to disable canonical mode and echo
10local of_attr = sys.tcgetattr(io.stdin)
11sys.setnonblock(io.stdin, true)
12sys.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
19local 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")
24end
25
26
27local rows, cols
28print("Change the terminal window size, press any key to exit")
29while 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
37end
diff --git a/luasystem-scm-0.rockspec b/luasystem-scm-0.rockspec
index ff5af61..00a442c 100644
--- a/luasystem-scm-0.rockspec
+++ b/luasystem-scm-0.rockspec
@@ -59,6 +59,8 @@ local function make_platform(plat)
59 'src/environment.c', 59 'src/environment.c',
60 'src/random.c', 60 'src/random.c',
61 'src/term.c', 61 'src/term.c',
62 'src/bitflags.c',
63 'src/wcwidth.c',
62 }, 64 },
63 defines = defines[plat], 65 defines = defines[plat],
64 libraries = libraries[plat], 66 libraries = libraries[plat],
diff --git a/spec/04-term_spec.lua b/spec/04-term_spec.lua
index a2034aa..57fb4d0 100644
--- a/spec/04-term_spec.lua
+++ b/spec/04-term_spec.lua
@@ -1,9 +1,21 @@
1-- Import the library that contains the environment-related functions
2local system = require("system") 1local system = require("system")
3require("spec.helpers") 2require("spec.helpers")
4 3
5describe("Terminal:", function() 4describe("Terminal:", function()
6 5
6 local wincodepage
7
8 setup(function()
9 wincodepage = system.getconsoleoutputcp()
10 assert(system.setconsoleoutputcp(system.CODEPAGE_UTF8)) -- set to UTF8
11 end)
12
13 teardown(function()
14 assert(system.setconsoleoutputcp(wincodepage))
15 end)
16
17
18
7 describe("isatty()", function() 19 describe("isatty()", function()
8 20
9 local newtmpfile = require("pl.path").tmpname 21 local newtmpfile = require("pl.path").tmpname
@@ -91,4 +103,798 @@ describe("Terminal:", function()
91 103
92 end) 104 end)
93 105
106
107
108 describe("getconsoleflags()", function()
109
110 win_it("returns the consoleflags #manual", function()
111 local flags, err = system.getconsoleflags(io.stdout)
112 assert.is_nil(err)
113 assert.is_userdata(flags)
114 assert.equals("bitflags:", tostring(flags):sub(1,9))
115 end)
116
117
118 nix_it("returns the consoleflags, always value 0", function()
119 local flags, err = system.getconsoleflags(io.stdout)
120 assert.is_nil(err)
121 assert.is_userdata(flags)
122 assert.equals("bitflags:", tostring(flags):sub(1,9))
123 assert.equals(0, flags:value())
124 end)
125
126
127 it("returns an error if called with an invalid argument", function()
128 assert.has.error(function()
129 system.getconsoleflags("invalid")
130 end, "bad argument #1 to 'getconsoleflags' (FILE* expected, got string)")
131 end)
132
133 end)
134
135
136
137 describe("setconsoleflags()", function()
138
139 win_it("sets the consoleflags #manual", function()
140 local old_flags = assert(system.getconsoleflags(io.stdout))
141 finally(function()
142 system.setconsoleflags(io.stdout, old_flags) -- ensure we restore the original ones
143 end)
144
145 local new_flags
146 if old_flags:has_all_of(system.COF_VIRTUAL_TERMINAL_PROCESSING) then
147 new_flags = old_flags - system.COF_VIRTUAL_TERMINAL_PROCESSING
148 else
149 new_flags = old_flags + system.COF_VIRTUAL_TERMINAL_PROCESSING
150 end
151
152 local success, err = system.setconsoleflags(io.stdout, new_flags)
153 assert.is_nil(err)
154 assert.is_true(success)
155
156 local updated_flags = assert(system.getconsoleflags(io.stdout))
157 assert.equals(new_flags:value(), updated_flags:value())
158 end)
159
160
161 nix_it("sets the consoleflags, always succeeds", function()
162 assert(system.setconsoleflags(io.stdout, system.getconsoleflags(io.stdout)))
163 end)
164
165
166 it("returns an error if called with an invalid argument", function()
167 assert.has.error(function()
168 system.setconsoleflags("invalid")
169 end, "bad argument #1 to 'setconsoleflags' (FILE* expected, got string)")
170 end)
171
172 end)
173
174
175
176 describe("tcgetattr()", function()
177
178 nix_it("gets the terminal flags #manual", function()
179 local flags, err = system.tcgetattr(io.stdin)
180 assert.is_nil(err)
181 assert.is_table(flags)
182 assert.equals("bitflags:", tostring(flags.iflag):sub(1,9))
183 assert.equals("bitflags:", tostring(flags.oflag):sub(1,9))
184 assert.equals("bitflags:", tostring(flags.lflag):sub(1,9))
185 assert.equals("bitflags:", tostring(flags.cflag):sub(1,9))
186 assert.not_equal(0, flags.iflag:value())
187 assert.not_equal(0, flags.oflag:value())
188 assert.not_equal(0, flags.lflag:value())
189 assert.not_equal(0, flags.cflag:value())
190 assert.is.table(flags.cc)
191 end)
192
193
194 win_it("gets the terminal flags, always 0", function()
195 local flags, err = system.tcgetattr(io.stdin)
196 assert.is_nil(err)
197 assert.is_table(flags)
198 assert.equals("bitflags:", tostring(flags.iflag):sub(1,9))
199 assert.equals("bitflags:", tostring(flags.oflag):sub(1,9))
200 assert.equals("bitflags:", tostring(flags.lflag):sub(1,9))
201 assert.equals("bitflags:", tostring(flags.cflag):sub(1,9))
202 assert.equals(0, flags.iflag:value())
203 assert.equals(0, flags.oflag:value())
204 assert.equals(0, flags.lflag:value())
205 assert.equals(0, flags.cflag:value())
206 assert.same({}, flags.cc)
207 end)
208
209
210 it("returns an error if called with an invalid argument", function()
211 assert.has.error(function()
212 system.tcgetattr("invalid")
213 end, "bad argument #1 to 'tcgetattr' (FILE* expected, got string)")
214 end)
215
216 end)
217
218
219
220 describe("tcsetattr()", function()
221
222 nix_it("sets the terminal flags, if called with flags #manual", function()
223 -- system.listtermflags(io.stdin)
224 local old_flags = assert(system.tcgetattr(io.stdin))
225 finally(function()
226 system.tcsetattr(io.stdin, system.TCSANOW, old_flags) -- ensure we restore the original ones
227 end)
228
229 local new_flags = assert(system.tcgetattr(io.stdin)) -- just get them again, and then update
230 -- change iflags
231 local flag_to_change = system.I_IGNCR
232 if new_flags.iflag:has_all_of(flag_to_change) then
233 new_flags.iflag = new_flags.iflag - flag_to_change
234 else
235 new_flags.iflag = new_flags.iflag + flag_to_change
236 end
237
238 -- change oflags
239 flag_to_change = system.O_OPOST
240 if new_flags.oflag:has_all_of(flag_to_change) then
241 new_flags.oflag = new_flags.oflag - flag_to_change
242 else
243 new_flags.oflag = new_flags.oflag + flag_to_change
244 end
245
246 -- change lflags
247 flag_to_change = system.L_ECHO
248 if new_flags.lflag:has_all_of(flag_to_change) then
249 new_flags.lflag = new_flags.lflag - flag_to_change
250 else
251 new_flags.lflag = new_flags.lflag + flag_to_change
252 end
253
254 assert(system.tcsetattr(io.stdin, system.TCSANOW, new_flags))
255
256 local updated_flags = assert(system.tcgetattr(io.stdin))
257 assert.equals(new_flags.iflag:value(), updated_flags.iflag:value())
258 assert.equals(new_flags.oflag:value(), updated_flags.oflag:value())
259 assert.equals(new_flags.lflag:value(), updated_flags.lflag:value())
260 end)
261
262
263 win_it("sets the terminal flags, if called with flags, always succeeds", function()
264 local success, err = system.tcsetattr(io.stdin, system.TCSANOW, system.tcgetattr(io.stdin))
265 assert.is_nil(err)
266 assert.is_true(success)
267 end)
268
269
270 it("returns an error if called with an invalid first argument", function()
271 assert.has.error(function()
272 system.tcsetattr("invalid", system.TCSANOW, {})
273 end, "bad argument #1 to 'tcsetattr' (FILE* expected, got string)")
274 end)
275
276
277 it("returns an error if called with an invalid second argument", function()
278 assert.has.error(function()
279 system.tcsetattr(io.stdin, "invalid", {})
280 end, "bad argument #2 to 'tcsetattr' (number expected, got string)")
281 end)
282
283
284 it("returns an error if called with an invalid third argument #manual", function()
285 assert.has.error(function()
286 system.tcsetattr(io.stdin, system.TCSANOW, "invalid")
287 end, "bad argument #3 to 'tcsetattr' (table expected, got string)")
288 end)
289
290
291 it("returns an error if iflag is not a bitflags object #manual", function()
292 local flags = assert(system.tcgetattr(io.stdin))
293 flags.iflag = 0
294 assert.has.error(function()
295 system.tcsetattr(io.stdin, system.TCSANOW, flags)
296 end, "bad argument #3, field 'iflag' must be a bitflag object")
297 end)
298
299
300 it("returns an error if oflag is not a bitflags object #manual", function()
301 local flags = assert(system.tcgetattr(io.stdin))
302 flags.oflag = 0
303 assert.has.error(function()
304 system.tcsetattr(io.stdin, system.TCSANOW, flags)
305 end, "bad argument #3, field 'oflag' must be a bitflag object")
306 end)
307
308
309 it("returns an error if lflag is not a bitflags object #manual", function()
310 local flags = assert(system.tcgetattr(io.stdin))
311 flags.lflag = 0
312 assert.has.error(function()
313 system.tcsetattr(io.stdin, system.TCSANOW, flags)
314 end, "bad argument #3, field 'lflag' must be a bitflag object")
315 end)
316
317 end)
318
319
320
321 describe("getconsolecp()", function()
322
323 win_it("gets the console codepage", function()
324 local cp, err = system.getconsolecp()
325 assert.is_nil(err)
326 assert.is_number(cp)
327 end)
328
329 nix_it("gets the console codepage, always 65001 (utf8)", function()
330 local cp, err = system.getconsolecp()
331 assert.is_nil(err)
332 assert.equals(65001, cp)
333 end)
334
335 end)
336
337
338
339 describe("setconsolecp()", function()
340
341 win_it("sets the console codepage", function()
342 local old_cp = assert(system.getconsolecp())
343 finally(function()
344 system.setconsolecp(old_cp) -- ensure we restore the original one
345 end)
346
347 local new_cp
348 if old_cp ~= system.CODEPAGE_UTF8 then
349 new_cp = system.CODEPAGE_UTF8 -- set to UTF8
350 else
351 new_cp = 850 -- another common one
352 end
353
354 local success, err = system.setconsolecp(new_cp)
355 assert.is_nil(err)
356 assert.is_true(success)
357
358 local updated_cp = assert(system.getconsolecp())
359 assert.equals(new_cp, updated_cp)
360 end)
361
362
363 nix_it("sets the console codepage, always succeeds", function()
364 assert(system.setconsolecp(850))
365 end)
366
367
368 it("returns an error if called with an invalid argument", function()
369 assert.has.error(function()
370 system.setconsolecp("invalid")
371 end, "bad argument #1 to 'setconsolecp' (number expected, got string)")
372 end)
373
374 end)
375
376
377
378 describe("getconsoleoutputcp()", function()
379
380 win_it("gets the console output codepage", function()
381 local cp, err = system.getconsoleoutputcp()
382 assert.is_nil(err)
383 assert.is_number(cp)
384 end)
385
386 nix_it("gets the console output codepage, always 65001 (utf8)", function()
387 local cp, err = system.getconsoleoutputcp()
388 assert.is_nil(err)
389 assert.equals(65001, cp)
390 end)
391
392 end)
393
394
395
396 describe("setconsoleoutputcp()", function()
397
398 win_it("sets the console output codepage", function()
399 local old_cp = assert(system.getconsoleoutputcp())
400 finally(function()
401 system.setconsoleoutputcp(old_cp) -- ensure we restore the original one
402 end)
403
404 local new_cp
405 if old_cp ~= system.CODEPAGE_UTF8 then
406 new_cp = system.CODEPAGE_UTF8 -- set to UTF8
407 else
408 new_cp = 850 -- another common one
409 end
410
411 local success, err = system.setconsoleoutputcp(new_cp)
412 assert.is_nil(err)
413 assert.is_true(success)
414
415 local updated_cp = assert(system.getconsoleoutputcp())
416 assert.equals(new_cp, updated_cp)
417 end)
418
419
420 nix_it("sets the console output codepage, always succeeds", function()
421 assert(system.setconsoleoutputcp(850))
422 end)
423
424
425 it("returns an error if called with an invalid argument", function()
426 assert.has.error(function()
427 system.setconsoleoutputcp("invalid")
428 end, "bad argument #1 to 'setconsoleoutputcp' (number expected, got string)")
429 end)
430
431 end)
432
433
434
435 describe("getnonblock()", function()
436
437 nix_it("gets the non-blocking flag", function()
438 local nb, err = system.getnonblock(io.stdin)
439 assert.is_nil(err)
440 assert.is_boolean(nb)
441 end)
442
443
444 win_it("gets the non-blocking flag, always false", function()
445 local nb, err = system.getnonblock(io.stdin)
446 assert.is_nil(err)
447 assert.is_false(nb)
448 end)
449
450
451 it("returns an error if called with an invalid argument", function()
452 assert.has.error(function()
453 system.getnonblock("invalid")
454 end, "bad argument #1 to 'getnonblock' (FILE* expected, got string)")
455 end)
456
457 end)
458
459
460
461 describe("setnonblock()", function()
462
463 nix_it("sets the non-blocking flag", function()
464 local old_nb = system.getnonblock(io.stdin)
465 assert.is.boolean(old_nb)
466
467 finally(function()
468 system.setnonblock(io.stdin, old_nb) -- ensure we restore the original one
469 end)
470
471 local new_nb = not old_nb
472
473 local success, err = system.setnonblock(io.stdin, new_nb)
474 assert.is_nil(err)
475 assert.is_true(success)
476
477 local updated_nb = assert(system.getnonblock(io.stdin))
478 assert.equals(new_nb, updated_nb)
479 end)
480
481
482 win_it("sets the non-blocking flag, always succeeds", function()
483 local success, err = system.setnonblock(io.stdin, true)
484 assert.is_nil(err)
485 assert.is_true(success)
486 end)
487
488
489 it("returns an error if called with an invalid argument", function()
490 assert.has.error(function()
491 system.setnonblock("invalid")
492 end, "bad argument #1 to 'setnonblock' (FILE* expected, got string)")
493 end)
494
495 end)
496
497
498
499 describe("termsize() #manual", function()
500
501 it("gets the terminal size", function()
502 local rows, columns = system.termsize()
503 assert.is_number(rows)
504 assert.is_number(columns)
505 end)
506
507 end)
508
509
510
511 describe("utf8cwidth()", function()
512
513 local ch1 = string.char(226, 130, 172) -- "€" single
514 local ch2 = string.char(240, 159, 154, 128) -- "🚀" double
515 local ch3 = string.char(228, 189, 160) -- "ä½ " double
516 local ch4 = string.char(229, 165, 189) -- "好" double
517
518 it("handles zero width characters", function()
519 assert.same({0}, {system.utf8cwidth("")}) -- empty string returns 0-size
520 assert.same({nil, 'Character width determination failed'}, {system.utf8cwidth("\a")}) -- bell character
521 assert.same({nil, 'Character width determination failed'}, {system.utf8cwidth("\27")}) -- escape character
522 end)
523
524 it("handles single width characters", function()
525 assert.same({1}, {system.utf8cwidth("a")})
526 assert.same({1}, {system.utf8cwidth(ch1)})
527 end)
528
529 it("handles double width characters", function()
530 assert.same({2}, {system.utf8cwidth(ch2)})
531 assert.same({2}, {system.utf8cwidth(ch3)})
532 assert.same({2}, {system.utf8cwidth(ch4)})
533 end)
534
535 it("returns the width of the first character in the string", function()
536 assert.same({nil, 'Character width determination failed'}, {system.utf8cwidth("\a" .. ch1)}) -- bell character + EURO
537 assert.same({1}, {system.utf8cwidth(ch1 .. ch2)})
538 assert.same({2}, {system.utf8cwidth(ch2 .. ch3 .. ch4)})
539 end)
540
541 end)
542
543
544
545 describe("utf8swidth()", function()
546
547 local ch1 = string.char(226, 130, 172) -- "€" single
548 local ch2 = string.char(240, 159, 154, 128) -- "🚀" double
549 local ch3 = string.char(228, 189, 160) -- "ä½ " double
550 local ch4 = string.char(229, 165, 189) -- "好" double
551
552 it("handles zero width characters", function()
553 assert.same({0}, {system.utf8swidth("")}) -- empty string returns 0-size
554 assert.same({nil, 'Character width determination failed'}, {system.utf8swidth("\a")}) -- bell character
555 assert.same({nil, 'Character width determination failed'}, {system.utf8swidth("\27")}) -- escape character
556 end)
557
558 it("handles multi-character UTF8 strings", function()
559 assert.same({15}, {system.utf8swidth("hello " .. ch1 .. ch2 .. " world")})
560 assert.same({16}, {system.utf8swidth("hello " .. ch3 .. ch4 .. " world")})
561 end)
562
563 end)
564
565
566
567 describe("termbackup() & termrestore()", function()
568
569 -- this is all Lua code, so testing one platform should be good enough
570 win_it("creates and restores a backup", function()
571 local backup = system.termbackup()
572
573 local old_cp = assert(system.getconsoleoutputcp())
574 finally(function()
575 system.setconsoleoutputcp(old_cp) -- ensure we restore the original one
576 end)
577
578 -- get the console page...
579 local new_cp
580 if old_cp ~= system.CODEPAGE_UTF8 then
581 new_cp = system.CODEPAGE_UTF8 -- set to UTF8
582 else
583 new_cp = 850 -- another common one
584 end
585
586 -- change the console page...
587 local success, err = system.setconsoleoutputcp(new_cp)
588 assert.is_nil(err)
589 assert.is_true(success)
590 -- ... and check it
591 local updated_cp = assert(system.getconsoleoutputcp())
592 assert.equals(new_cp, updated_cp)
593
594 -- restore the console page
595 system.termrestore(backup)
596 local restored_cp = assert(system.getconsoleoutputcp())
597 assert.equals(old_cp, restored_cp)
598 end)
599
600
601 it("termrestore() fails on bad input", function()
602 assert.has.error(function()
603 system.termrestore("invalid")
604 end, "arg #1 to termrestore, expected backup table, got string")
605 end)
606
607 end)
608
609
610
611 describe("termwrap()", function()
612
613 local old_backup
614 local old_restore
615 local result
616
617 setup(function()
618 old_backup = system.termbackup
619 old_restore = system.termrestore
620 system.termbackup = function()
621 table.insert(result,"backup")
622 end
623
624 system.termrestore = function()
625 table.insert(result,"restore")
626 end
627 end)
628
629
630 before_each(function()
631 result = {}
632 end)
633
634
635 teardown(function()
636 system.termbackup = old_backup
637 system.termrestore = old_restore
638 end)
639
640
641
642 it("calls both backup and restore", function()
643 system.termwrap(function()
644 table.insert(result,"wrapped")
645 end)()
646
647 assert.are.same({"backup", "wrapped", "restore"}, result)
648 end)
649
650
651 it("passes all args", function()
652 system.termwrap(function(...)
653 table.insert(result,{...})
654 end)(1, 2, 3)
655
656 assert.are.same({"backup", {1,2,3}, "restore"}, result)
657 end)
658
659
660 it("returns all results", function()
661 local a, b, c = system.termwrap(function(...)
662 return 1, nil, 3 -- ensure nil is passed as well
663 end)()
664
665 assert.are.same({"backup", "restore"}, result)
666 assert.equals(1, a)
667 assert.is_nil(b)
668 assert.equals(3, c)
669 end)
670
671 end)
672
673
674
675 describe("autotermrestore()", function()
676
677 local old_backup
678 local old_restore
679 local result
680
681
682 before_each(function()
683 _G._TEST = true
684
685 package.loaded["system"] = nil
686 system = require("system")
687
688 old_backup = system.termbackup
689 old_restore = system.termrestore
690 system.termbackup = function(...)
691 table.insert(result,"backup")
692 return old_backup(...)
693 end
694
695 system.termrestore = function(...)
696 table.insert(result,"restore")
697 return old_restore(...)
698 end
699
700 result = {}
701 end)
702
703
704 after_each(function()
705 -- system.termbackup = old_backup
706 -- system.termrestore = old_restore
707 _G._TEST = false
708
709 package.loaded["system"] = nil
710 system = require("system")
711 end)
712
713
714
715 it("calls backup", function()
716 local ok, err = system.autotermrestore()
717 assert.is_nil(err)
718 assert.is_true(ok)
719
720 assert.are.same({"backup"}, result)
721 end)
722
723
724 it("returns an error on the second call", function()
725 local ok, err = system.autotermrestore()
726 assert.is_nil(err)
727 assert.is_true(ok)
728
729 local ok, err = system.autotermrestore()
730 assert.are.equal("global terminal backup was already set up", err)
731 assert.is_nil(ok)
732 end)
733
734
735 it("calls restore upon being garbage collected", function()
736 local ok, err = system.autotermrestore()
737 assert.is_nil(err)
738 assert.is_true(ok)
739
740 -- ensure tables from previous tests are GC'ed
741 collectgarbage()
742 collectgarbage()
743 -- clear references
744 result = {}
745 system._reset_global_backup()
746 collectgarbage()
747 collectgarbage()
748
749 assert.are.same({"restore"}, result)
750 end)
751
752 end)
753
754
755
756 describe("keyboard input", function()
757
758 local old_readkey = system._readkey
759 local current_buffer
760 local function setbuffer(str)
761 assert(type(str) == "string", "setbuffer() expects a string")
762 if str == "" then
763 current_buffer = nil
764 else
765 current_buffer = str
766 end
767 end
768
769
770 setup(function()
771 system._readkey = function()
772 if not current_buffer then
773 return nil
774 end
775 local ch = current_buffer:byte(1, 1)
776 if #current_buffer == 1 then
777 current_buffer = nil
778 else
779 current_buffer = current_buffer:sub(2, -1)
780 end
781 return ch
782 end
783 end)
784
785
786 teardown(function()
787 system._readkey = old_readkey
788 end)
789
790
791
792 describe("readkey()", function()
793
794 it("fails without a timeout", function()
795 assert.has.error(function()
796 system.readkey()
797 end, "arg #1 to readkey, expected timeout in seconds, got nil")
798 end)
799
800
801 it("reads a single byte for single-byte characters", function()
802 setbuffer("abc")
803 assert.equals(string.byte("a"), system.readkey(0))
804 assert.equals(string.byte("b"), system.readkey(0))
805 assert.equals(string.byte("c"), system.readkey(0))
806 end)
807
808
809 it("reads a single byte for multi-byte chars", function()
810 setbuffer(string.char(226, 130, 172) .. -- "€" single
811 string.char(240, 159, 154, 128)) -- "🚀" double
812 assert.equals(226, system.readkey(0))
813 assert.equals(130, system.readkey(0))
814 assert.equals(172, system.readkey(0))
815 assert.equals(240, system.readkey(0))
816 assert.equals(159, system.readkey(0))
817 assert.equals(154, system.readkey(0))
818 assert.equals(128, system.readkey(0))
819 end)
820
821
822 it("times out", function()
823 setbuffer("")
824 local timing = system.gettime()
825 assert.same({ nil, "timeout" }, { system.readkey(0.5) })
826
827 timing = system.gettime() - timing
828 -- assert.is.near(0.5, timing, 0.1) -- this works in CI for Unix and Windows, but not MacOS (locally it passes)
829 assert.is.near(1, timing, 0.5) -- this also works for MacOS in CI
830 end)
831
832 end)
833
834
835
836 describe("readansi()", function()
837
838 it("fails without a timeout", function()
839 assert.has.error(function()
840 system.readansi()
841 end, "arg #1 to readansi, expected timeout in seconds, got nil")
842 end)
843
844
845 it("reads a single byte for single-byte characters", function()
846 setbuffer("abc")
847 assert.are.same({"a", "char"}, {system.readansi(0)})
848 assert.are.same({"b", "char"}, {system.readansi(0)})
849 assert.are.same({"c", "char"}, {system.readansi(0)})
850 end)
851
852
853 it("reads a multi-byte characters one at a time", function()
854 setbuffer(string.char(226, 130, 172) .. -- "€" single
855 string.char(240, 159, 154, 128)) -- "🚀" double
856 assert.are.same({"€", "char"}, {system.readansi(0)})
857 assert.are.same({"🚀", "char"}, {system.readansi(0)})
858 end)
859
860
861 it("reads ANSI escape sequences, marked by '<esc>['", function()
862 setbuffer("\27[A\27[B\27[C\27[D")
863 assert.are.same({"\27[A", "ansi"}, {system.readansi(0)})
864 assert.are.same({"\27[B", "ansi"}, {system.readansi(0)})
865 assert.are.same({"\27[C", "ansi"}, {system.readansi(0)})
866 assert.are.same({"\27[D", "ansi"}, {system.readansi(0)})
867 end)
868
869
870 it("reads ANSI escape sequences, marked by '<esc>O'", function()
871 setbuffer("\27OA\27OB\27OC\27OD")
872 assert.are.same({"\27OA", "ansi"}, {system.readansi(0)})
873 assert.are.same({"\27OB", "ansi"}, {system.readansi(0)})
874 assert.are.same({"\27OC", "ansi"}, {system.readansi(0)})
875 assert.are.same({"\27OD", "ansi"}, {system.readansi(0)})
876 end)
877
878
879 it("returns a single <esc> character if no sequence is found", function()
880 setbuffer("\27\27[A")
881 assert.are.same({"\27", "char"}, {system.readansi(0)})
882 assert.are.same({"\27[A", "ansi"}, {system.readansi(0)})
883 end)
884
885
886 it("times out", function()
887 setbuffer("")
888 local timing = system.gettime()
889 assert.same({ nil, "timeout" }, { system.readansi(0.5) })
890
891 timing = system.gettime() - timing
892 -- assert.is.near(0.5, timing, 0.1) -- this works in CI for Unix and Windows, but not MacOS (locally it passes)
893 assert.is.near(1, timing, 0.5) -- this also works for MacOS in CI
894 end)
895
896 end)
897
898 end)
899
94end) 900end)
diff --git a/spec/05-bitflags_spec.lua b/spec/05-bitflags_spec.lua
new file mode 100644
index 0000000..8eea27f
--- /dev/null
+++ b/spec/05-bitflags_spec.lua
@@ -0,0 +1,114 @@
1describe("BitFlags library", function()
2
3 local sys = require("system")
4
5 it("creates new flag objects", function()
6 local bf = sys.bitflag(255)
7 assert.is_not_nil(bf)
8 assert.are.equal(255, bf:value())
9 assert.is.userdata(bf)
10 end)
11
12 it("converts to a hex string", function()
13 local bf = sys.bitflag(255)
14 assert.are.equal("bitflags: 255", tostring(bf))
15 end)
16
17 it("handles OR/ADD operations", function()
18 -- one at a time
19 local bf1 = sys.bitflag(1) -- b0001
20 local bf2 = sys.bitflag(2) -- b0010
21 local bf3 = bf1 + bf2 -- b0011
22 assert.are.equal(3, bf3:value())
23 -- multiple at once
24 local bf4 = sys.bitflag(4+8) -- b1100
25 local bf5 = bf3 + bf4 -- b1111
26 assert.are.equal(15, bf5:value())
27 -- multiple that were already set
28 local bf6 = sys.bitflag(15) -- b1111
29 local bf7 = sys.bitflag(8+2) -- b1010
30 local bf8 = bf6 + bf7 -- b1111
31 assert.are.equal(15, bf8:value())
32 end)
33
34 it("handles AND-NOT/SUBSTRACT operations", function()
35 -- one at a time
36 local bf1 = sys.bitflag(3) -- b0011
37 local bf2 = sys.bitflag(1) -- b0001
38 local bf3 = bf1 - bf2 -- b0010
39 assert.are.equal(2, bf3:value())
40 -- multiple at once
41 local bf4 = sys.bitflag(15) -- b1111
42 local bf5 = sys.bitflag(8+2) -- b1010
43 local bf6 = bf4 - bf5 -- b0101
44 assert.are.equal(5, bf6:value())
45 -- multiple that were not set
46 local bf7 = sys.bitflag(3) -- b0011
47 local bf8 = sys.bitflag(15) -- b1111
48 local bf9 = bf7 - bf8 -- b0000
49 assert.are.equal(0, bf9:value())
50 end)
51
52 it("checks for equality", function()
53 local bf1 = sys.bitflag(4)
54 local bf2 = sys.bitflag(4)
55 local bf3 = sys.bitflag(5)
56 assert.is.True(bf1 == bf2)
57 assert.is.False(bf1 == bf3)
58 end)
59
60 it("indexes bits correctly", function()
61 local bf = sys.bitflag(4) -- b100
62 assert.is_true(bf[2])
63 assert.is_false(bf[1])
64 end)
65
66 it("errors on reading invalid bit indexes", function()
67 local bf = sys.bitflag(4)
68 assert.has_error(function() return bf[-10] end, "index out of range")
69 assert.has_error(function() return bf[10000] end, "index out of range")
70 assert.has_no_error(function() return bf.not_a_number end)
71 end)
72
73 it("sets and clears bits correctly", function()
74 local bf = sys.bitflag(8) -- b1000
75 bf[1] = true
76 assert.is_true(bf[1]) -- b1010
77 assert.equals(10, bf:value())
78 bf[1] = false
79 assert.is_false(bf[1]) -- b1000
80 assert.equals(8, bf:value())
81 end)
82
83 it("errors on setting invalid bit indexes", function()
84 local bf = sys.bitflag(0)
85 assert.has_error(function() bf[-10] = true end, "index out of range")
86 assert.has_error(function() bf[10000] = true end, "index out of range")
87 assert.has_error(function() bf.not_a_number = true end, "index must be a number")
88 end)
89
90 it("checks for a subset using 'has_all_of'", function()
91 local bf1 = sys.bitflag(3) -- b0011
92 local bf2 = sys.bitflag(3) -- b0011
93 local bf3 = sys.bitflag(15) -- b1111
94 local bf0 = sys.bitflag(0) -- b0000
95 assert.is_true(bf1:has_all_of(bf2)) -- equal
96 assert.is_true(bf3:has_all_of(bf1)) -- is a subset, and has more flags
97 assert.is_false(bf1:has_all_of(bf3)) -- not a subset, bf3 has more flags
98 assert.is_false(bf1:has_all_of(bf0)) -- bf0 is unset, always returns false
99 end)
100
101 it("checks for a subset using 'has_any_of'", function()
102 local bf1 = sys.bitflag(3) -- b0011
103 local bf2 = sys.bitflag(3) -- b0011
104 local bf3 = sys.bitflag(7) -- b0111
105 local bf4 = sys.bitflag(8) -- b1000
106 local bf0 = sys.bitflag(0) -- b0000
107 assert.is_true(bf1:has_any_of(bf2)) -- equal
108 assert.is_true(bf3:has_any_of(bf1)) -- is a subset, and has more flags
109 assert.is_false(bf3:has_any_of(bf4)) -- no overlap in flags
110 assert.is_true(bf1:has_any_of(bf3)) -- not a subset, bf3 has more flags but still some overlap
111 assert.is_false(bf1:has_all_of(bf0)) -- bf0 is unset, always returns false
112 end)
113
114end)
diff --git a/src/bitflags.c b/src/bitflags.c
new file mode 100644
index 0000000..e397918
--- /dev/null
+++ b/src/bitflags.c
@@ -0,0 +1,270 @@
1/// Bitflags module.
2// The bitflag object makes it easy to manipulate flags in a bitmask.
3//
4// It has metamethods that do the hard work, adding flags sets them, substracting
5// unsets them. Comparing flags checks if all flags in the second set are also set
6// in the first set. The `has` method checks if all flags in the second set are
7// also set in the first set, but behaves slightly different.
8//
9// Indexing allows checking values or setting them by bit index (eg. 0-7 for flags
10// in the first byte).
11//
12// _NOTE_: unavailable flags (eg. Windows flags on a Posix system) should not be
13// omitted, but be assigned a value of 0. This is because the `has` method will
14// return `false` if the flags are checked and the value is 0.
15//
16// See `system.bitflag` (the constructor) for extensive examples on usage.
17// @classmod bitflags
18#include "bitflags.h"
19
20#define BITFLAGS_MT_NAME "LuaSystem.BitFlags"
21
22typedef struct {
23 LSBF_BITFLAG flags;
24} LS_BitFlags;
25
26/// Bit flags.
27// Bitflag objects can be used to easily manipulate and compare bit flags.
28// These are primarily for use with the terminal functions, but can be used
29// in other places as well.
30// @section bitflags
31
32
33// pushes a new LS_BitFlags object with the given value onto the stack
34void lsbf_pushbitflags(lua_State *L, LSBF_BITFLAG value) {
35 LS_BitFlags *obj = (LS_BitFlags *)lua_newuserdata(L, sizeof(LS_BitFlags));
36 if (!obj) luaL_error(L, "Memory allocation failed");
37 luaL_getmetatable(L, BITFLAGS_MT_NAME);
38 lua_setmetatable(L, -2);
39 obj->flags = value;
40}
41
42// gets the LS_BitFlags value at the given index. Returns a Lua error if it is not
43// a LS_BitFlags object.
44LSBF_BITFLAG lsbf_checkbitflags(lua_State *L, int index) {
45 LS_BitFlags *obj = (LS_BitFlags *)luaL_checkudata(L, index, BITFLAGS_MT_NAME);
46 return obj->flags;
47}
48
49// Validates that the given index is a table containing a field 'fieldname'
50// which is a bitflag object and returns its value.
51// If the index is not a table or the field is not a bitflag object, a Lua
52// error is raised. If the bitflag is not present, the default value is returned.
53// The stack remains unchanged.
54LSBF_BITFLAG lsbf_checkbitflagsfield(lua_State *L, int index, const char *fieldname, LSBF_BITFLAG default_value) {
55 luaL_checktype(L, index, LUA_TTABLE);
56 lua_getfield(L, index, fieldname);
57
58 // if null, return default value
59 if (lua_isnil(L, -1)) {
60 lua_pop(L, 1);
61 return default_value;
62 }
63
64 // check to bitflags
65 LS_BitFlags *obj = luaL_testudata(L, -1, BITFLAGS_MT_NAME);
66 if (obj == NULL) {
67 lua_pop(L, 1);
68 return luaL_error(L, "bad argument #%d, field '%s' must be a bitflag object", index, fieldname);
69 }
70 LSBF_BITFLAG value = obj->flags;
71 lua_pop(L, 1);
72 return value;
73}
74
75/***
76Creates a new bitflag object from the given value.
77@function system.bitflag
78@tparam[opt=0] number value the value to create the bitflag object from.
79@treturn bitflag bitflag object with the given values set.
80@usage
81local sys = require 'system'
82local flags = sys.bitflag(2) -- b0010
83
84-- get state of individual bits
85print(flags[0]) -- false
86print(flags[1]) -- true
87
88-- set individual bits
89flags[0] = true -- b0011
90print(flags:value()) -- 3
91print(flags) -- "bitflags: 3"
92
93-- adding flags (bitwise OR)
94local flags1 = sys.bitflag(1) -- b0001
95local flags2 = sys.bitflag(2) -- b0010
96local flags3 = flags1 + flags2 -- b0011
97
98-- substracting flags (bitwise AND NOT)
99print(flags3:value()) -- 3
100flag3 = flag3 - flag3 -- b0000
101print(flags3:value()) -- 0
102
103-- comparing flags
104local flags4 = sys.bitflag(7) -- b0111
105local flags5 = sys.bitflag(255) -- b11111111
106print(flags5 ~= flags4) -- true, not the same flags
107local flags6 = sys.bitflag(7) -- b0111
108print(flags6 == flags4) -- true, same flags
109
110-- comparison of subsets
111local flags7 = sys.bitflag(0) -- b0000
112local flags8 = sys.bitflag(3) -- b0011
113local flags9 = sys.bitflag(7) -- b0111
114print(flags9:has_all_of(flags8)) -- true, flags8 bits are all set in flags9
115print(flags8:has_any_of(flags9)) -- true, some of flags9 bits are set in flags8
116print(flags8:has_all_of(flags7)) -- false, flags7 (== 0) is not set in flags8
117*/
118static int lsbf_new(lua_State *L) {
119 LSBF_BITFLAG flags = 0;
120 if (lua_gettop(L) > 0) {
121 flags = luaL_checkinteger(L, 1);
122 }
123 lsbf_pushbitflags(L, flags);
124 return 1;
125}
126
127/***
128Retrieves the numeric value of the bitflag object.
129@function bitflag:value
130@treturn number the numeric value of the bitflags.
131@usage
132local sys = require 'system'
133local flags = sys.bitflag() -- b0000
134flags[0] = true -- b0001
135flags[2] = true -- b0101
136print(flags:value()) -- 5
137*/
138static int lsbf_value(lua_State *L) {
139 lua_pushinteger(L, lsbf_checkbitflags(L, 1));
140 return 1;
141}
142
143static int lsbf_tostring(lua_State *L) {
144 lua_pushfstring(L, "bitflags: %d", lsbf_checkbitflags(L, 1));
145 return 1;
146}
147
148static int lsbf_add(lua_State *L) {
149 lsbf_pushbitflags(L, lsbf_checkbitflags(L, 1) | lsbf_checkbitflags(L, 2));
150 return 1;
151}
152
153static int lsbf_sub(lua_State *L) {
154 lsbf_pushbitflags(L, lsbf_checkbitflags(L, 1) & ~lsbf_checkbitflags(L, 2));
155 return 1;
156}
157
158static int lsbf_eq(lua_State *L) {
159 lua_pushboolean(L, lsbf_checkbitflags(L, 1) == lsbf_checkbitflags(L, 2));
160 return 1;
161}
162
163/***
164Checks if all the flags in the given subset are set.
165If the flags to check has a value `0`, it will always return `false`. So if there are flags that are
166unsupported on a platform, they can be set to 0 and the `has_all_of` function will
167return `false` if the flags are checked.
168@function bitflag:has_all_of
169@tparam bitflag subset the flags to check for.
170@treturn boolean true if all the flags are set, false otherwise.
171@usage
172local sys = require 'system'
173local flags = sys.bitflag(12) -- b1100
174local myflags = sys.bitflag(15) -- b1111
175print(flags:has_all_of(myflags)) -- false, not all bits in myflags are set in flags
176print(myflags:has_all_of(flags)) -- true, all bits in flags are set in myflags
177*/
178static int lsbf_has_all_of(lua_State *L) {
179 LSBF_BITFLAG a = lsbf_checkbitflags(L, 1);
180 LSBF_BITFLAG b = lsbf_checkbitflags(L, 2);
181 // Check if all bits in b are also set in a, and b is not 0
182 lua_pushboolean(L, (a & b) == b && b != 0);
183 return 1;
184}
185
186/***
187Checks if any of the flags in the given subset are set.
188If the flags to check has a value `0`, it will always return `false`. So if there are flags that are
189unsupported on a platform, they can be set to 0 and the `has_any_of` function will
190return `false` if the flags are checked.
191@function bitflag:has_any_of
192@tparam bitflag subset the flags to check for.
193@treturn boolean true if any of the flags are set, false otherwise.
194@usage
195local sys = require 'system'
196local flags = sys.bitflag(12) -- b1100
197local myflags = sys.bitflag(7) -- b0111
198print(flags:has_any_of(myflags)) -- true, some bits in myflags are set in flags
199print(myflags:has_any_of(flags)) -- true, some bits in flags are set in myflags
200*/
201static int lsbf_has_any_of(lua_State *L) {
202 LSBF_BITFLAG a = lsbf_checkbitflags(L, 1);
203 LSBF_BITFLAG b = lsbf_checkbitflags(L, 2);
204 // Check if any bits in b are set in a
205 lua_pushboolean(L, (a & b) != 0);
206 return 1;
207}
208
209static int lsbf_index(lua_State *L) {
210 if (!lua_isnumber(L, 2)) {
211 // the parameter isn't a number, just lookup the key in the metatable
212 lua_getmetatable(L, 1);
213 lua_pushvalue(L, 2);
214 lua_gettable(L, -2);
215 return 1;
216 }
217
218 int index = luaL_checkinteger(L, 2);
219 if (index < 0 || index >= sizeof(LSBF_BITFLAG) * 8) {
220 return luaL_error(L, "index out of range");
221 }
222 lua_pushboolean(L, (lsbf_checkbitflags(L, 1) & (1 << index)) != 0);
223 return 1;
224}
225
226static int lsbf_newindex(lua_State *L) {
227 LS_BitFlags *obj = (LS_BitFlags *)luaL_checkudata(L, 1, BITFLAGS_MT_NAME);
228
229 if (!lua_isnumber(L, 2)) {
230 return luaL_error(L, "index must be a number");
231 }
232 int index = luaL_checkinteger(L, 2);
233 if (index < 0 || index >= sizeof(LSBF_BITFLAG) * 8) {
234 return luaL_error(L, "index out of range");
235 }
236
237 luaL_checkany(L, 3);
238 if (lua_toboolean(L, 3)) {
239 obj->flags |= (1 << index);
240 } else {
241 obj->flags &= ~(1 << index);
242 }
243 return 0;
244}
245
246static const struct luaL_Reg lsbf_funcs[] = {
247 {"bitflag", lsbf_new},
248 {NULL, NULL}
249};
250
251static const struct luaL_Reg lsbf_methods[] = {
252 {"value", lsbf_value},
253 {"has_all_of", lsbf_has_all_of},
254 {"has_any_of", lsbf_has_any_of},
255 {"__tostring", lsbf_tostring},
256 {"__add", lsbf_add},
257 {"__sub", lsbf_sub},
258 {"__eq", lsbf_eq},
259 {"__index", lsbf_index},
260 {"__newindex", lsbf_newindex},
261 {NULL, NULL}
262};
263
264void bitflags_open(lua_State *L) {
265 luaL_newmetatable(L, BITFLAGS_MT_NAME);
266 luaL_setfuncs(L, lsbf_methods, 0);
267 lua_pop(L, 1);
268
269 luaL_setfuncs(L, lsbf_funcs, 0);
270}
diff --git a/src/bitflags.h b/src/bitflags.h
new file mode 100644
index 0000000..f16b041
--- /dev/null
+++ b/src/bitflags.h
@@ -0,0 +1,30 @@
1#ifndef LSBITFLAGS_H
2#define LSBITFLAGS_H
3
4#include <lua.h>
5#include "compat.h"
6#include <lauxlib.h>
7#include <stdlib.h>
8
9// type used to store the bitflags
10#define LSBF_BITFLAG lua_Integer
11
12// Validates that the given index is a bitflag object and returns its value.
13// If the index is not a bitflag object, a Lua error is raised.
14// The value will be left on the stack.
15LSBF_BITFLAG lsbf_checkbitflags(lua_State *L, int index);
16
17
18// Validates that the given index is a table containing a field 'fieldname'
19// which is a bitflag object and returns its value.
20// If the index is not a table or the field is not a bitflag object, a Lua
21// error is raised. If the bitflag is not present, the default value is returned.
22// The stack remains unchanged.
23LSBF_BITFLAG lsbf_checkbitflagsfield(lua_State *L, int index, const char *fieldname, LSBF_BITFLAG default_value);
24
25
26// Pushes a new bitflag object with the given value onto the stack.
27// Might raise a Lua error if memory allocation fails.
28void lsbf_pushbitflags(lua_State *L, LSBF_BITFLAG value);
29
30#endif
diff --git a/src/compat.c b/src/compat.c
index 6f98854..2d2bec9 100644
--- a/src/compat.c
+++ b/src/compat.c
@@ -14,4 +14,20 @@ void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup) {
14 } 14 }
15 lua_pop(L, nup); /* remove upvalues */ 15 lua_pop(L, nup); /* remove upvalues */
16} 16}
17
18void *luaL_testudata(lua_State *L, int ud, const char *tname) {
19 void *p = lua_touserdata(L, ud);
20 if (p != NULL) { /* Check for userdata */
21 if (lua_getmetatable(L, ud)) { /* Does it have a metatable? */
22 lua_getfield(L, LUA_REGISTRYINDEX, tname); /* Get metatable we're looking for */
23 if (lua_rawequal(L, -1, -2)) { /* Compare metatables */
24 lua_pop(L, 2); /* Remove metatables from stack */
25 return p;
26 }
27 lua_pop(L, 2); /* Remove metatables from stack */
28 }
29 }
30 return NULL; /* Return NULL if check fails */
31}
32
17#endif 33#endif
diff --git a/src/compat.h b/src/compat.h
index 35f9ef2..2033aa3 100644
--- a/src/compat.h
+++ b/src/compat.h
@@ -6,6 +6,7 @@
6 6
7#if LUA_VERSION_NUM == 501 7#if LUA_VERSION_NUM == 501
8void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup); 8void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup);
9void *luaL_testudata(lua_State *L, int ud, const char *tname);
9#endif 10#endif
10 11
11 12
@@ -13,6 +14,17 @@ void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup);
13#include <sys/types.h> 14#include <sys/types.h>
14#endif 15#endif
15 16
17// Windows compatibility; define DWORD and TRUE/FALSE on non-Windows
18#ifndef _WIN32
19#ifndef DWORD
20#define DWORD unsigned long
21#endif
22#ifndef TRUE
23#define TRUE 1
24#define FALSE 0
25#endif
26#endif
27
16#ifdef _MSC_VER 28#ifdef _MSC_VER
17// MSVC Windows doesn't have ssize_t, so we define it here 29// MSVC Windows doesn't have ssize_t, so we define it here
18#if SIZE_MAX == UINT_MAX 30#if SIZE_MAX == UINT_MAX
diff --git a/src/core.c b/src/core.c
index 729023f..d233ecc 100644
--- a/src/core.c
+++ b/src/core.c
@@ -16,6 +16,7 @@ void time_open(lua_State *L);
16void environment_open(lua_State *L); 16void environment_open(lua_State *L);
17void random_open(lua_State *L); 17void random_open(lua_State *L);
18void term_open(lua_State *L); 18void term_open(lua_State *L);
19void bitflags_open(lua_State *L);
19 20
20/*------------------------------------------------------------------------- 21/*-------------------------------------------------------------------------
21 * Initializes all library modules. 22 * Initializes all library modules.
@@ -32,6 +33,7 @@ LUAEXPORT int luaopen_system_core(lua_State *L) {
32 lua_pushboolean(L, 0); 33 lua_pushboolean(L, 0);
33#endif 34#endif
34 lua_rawset(L, -3); 35 lua_rawset(L, -3);
36 bitflags_open(L); // must be first, used by others
35 time_open(L); 37 time_open(L);
36 random_open(L); 38 random_open(L);
37 term_open(L); 39 term_open(L);
diff --git a/src/environment.c b/src/environment.c
index 5f1c3da..ab5dd92 100644
--- a/src/environment.c
+++ b/src/environment.c
@@ -1,4 +1,8 @@
1/// @submodule system 1/// @module system
2
3/// Environment.
4// @section environment
5
2#include <lua.h> 6#include <lua.h>
3#include <lauxlib.h> 7#include <lauxlib.h>
4#include "compat.h" 8#include "compat.h"
diff --git a/src/random.c b/src/random.c
index 90fb3f2..e55461a 100644
--- a/src/random.c
+++ b/src/random.c
@@ -1,4 +1,9 @@
1/// @submodule system 1/// @module system
2
3/// Random.
4// @section random
5
6
2#include <lua.h> 7#include <lua.h>
3#include <lauxlib.h> 8#include <lauxlib.h>
4#include "compat.h" 9#include "compat.h"
diff --git a/src/term.c b/src/term.c
index 2adb1e9..d8cc38e 100644
--- a/src/term.c
+++ b/src/term.c
@@ -1,37 +1,1158 @@
1/// @submodule system 1/// @module system
2
3/// Terminal.
4// Unix: see https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios-termios3-and-stty/
5//
6// Windows: see https://learn.microsoft.com/en-us/windows/console/console-reference
7// @section terminal
8
2#include <lua.h> 9#include <lua.h>
3#include <lauxlib.h> 10#include <lauxlib.h>
4#include <lualib.h> 11#include <lualib.h>
5#include "compat.h" 12#include "compat.h"
13#include "bitflags.h"
6 14
7#ifndef _MSC_VER 15#ifndef _MSC_VER
8# include <unistd.h> 16# include <unistd.h>
9#endif 17#endif
10 18
19#ifdef _WIN32
20# include <windows.h>
21# include <locale.h>
22#else
23# include <termios.h>
24# include <string.h>
25# include <errno.h>
26# include <fcntl.h>
27# include <sys/ioctl.h>
28# include <unistd.h>
29# include <wchar.h>
30# include <locale.h>
31#endif
32
33
34// Windows does not have a wcwidth function, so we use compatibilty code from
35// http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c by Markus Kuhn
36#include "wcwidth.h"
37
38
39#ifdef _WIN32
40// after an error is returned, GetLastError() result can be passed to this function to get a string
41// representation of the error on the stack.
42// result will be nil+error on the stack, always 2 results.
43static void termFormatError(lua_State *L, DWORD errorCode, const char* prefix) {
44//static void FormatErrorAndReturn(lua_State *L, DWORD errorCode, const char* prefix) {
45 LPSTR messageBuffer = NULL;
46 FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
47 NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);
48
49 lua_pushnil(L);
50 if (messageBuffer) {
51 if (prefix) {
52 lua_pushfstring(L, "%s: %s", prefix, messageBuffer);
53 } else {
54 lua_pushstring(L, messageBuffer);
55 }
56 LocalFree(messageBuffer);
57 } else {
58 lua_pushfstring(L, "%sError code %d", prefix ? prefix : "", errorCode);
59 }
60}
61#else
62static int pusherror(lua_State *L, const char *info)
63{
64 lua_pushnil(L);
65 if (info==NULL)
66 lua_pushstring(L, strerror(errno));
67 else
68 lua_pushfstring(L, "%s: %s", info, strerror(errno));
69 lua_pushinteger(L, errno);
70 return 3;
71}
72#endif
11 73
12/*** 74/***
13Checks if a file-handle is a TTY. 75Checks if a file-handle is a TTY.
14 76
15@function isatty 77@function isatty
16@tparam file file the file-handle to check 78@tparam file file the file-handle to check, one of `io.stdin`, `io.stdout`, `io.stderr`.
17@treturn boolean true if the file is a tty 79@treturn boolean true if the file is a tty
80@usage
81local system = require('system')
82if system.isatty(io.stdin) then
83 -- enable ANSI coloring etc on Windows, does nothing in Posix.
84 local flags = system.getconsoleflags(io.stdout)
85 system.setconsoleflags(io.stdout, flags + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
86end
18*/ 87*/
19static int lua_isatty(lua_State* L) { 88static int lst_isatty(lua_State* L) {
20 FILE **fh = (FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE); 89 FILE **fh = (FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE);
21 lua_pushboolean(L, isatty(fileno(*fh))); 90 lua_pushboolean(L, isatty(fileno(*fh)));
22 return 1; 91 return 1;
23} 92}
24 93
25 94
95/*-------------------------------------------------------------------------
96 * Windows Get/SetConsoleMode functions
97 *-------------------------------------------------------------------------*/
26 98
27static luaL_Reg func[] = { 99typedef struct ls_RegConst {
28 { "isatty", lua_isatty }, 100 const char *name;
29 { NULL, NULL } 101 DWORD value;
102} ls_RegConst;
103
104// Define a macro to check if a constant is defined and set it to 0 if not.
105// This is needed because some flags are not defined on all platforms. So we
106// still export the constants, but they will be all 0, and hence not do anything.
107#ifdef _WIN32
108#define CHECK_WIN_FLAG_OR_ZERO(flag) flag
109#define CHECK_NIX_FLAG_OR_ZERO(flag) 0
110#else
111#define CHECK_WIN_FLAG_OR_ZERO(flag) 0
112#define CHECK_NIX_FLAG_OR_ZERO(flag) flag
113#endif
114
115// Export Windows constants to Lua
116static const struct ls_RegConst win_console_in_flags[] = {
117 // Console Input Flags
118 {"CIF_ECHO_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_ECHO_INPUT)},
119 {"CIF_INSERT_MODE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_INSERT_MODE)},
120 {"CIF_LINE_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_LINE_INPUT)},
121 {"CIF_MOUSE_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_MOUSE_INPUT)},
122 {"CIF_PROCESSED_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_PROCESSED_INPUT)},
123 {"CIF_QUICK_EDIT_MODE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_QUICK_EDIT_MODE)},
124 {"CIF_WINDOW_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_WINDOW_INPUT)},
125 {"CIF_VIRTUAL_TERMINAL_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_VIRTUAL_TERMINAL_INPUT)},
126 {"CIF_EXTENDED_FLAGS", CHECK_WIN_FLAG_OR_ZERO(ENABLE_EXTENDED_FLAGS)},
127 {"CIF_AUTO_POSITION", CHECK_WIN_FLAG_OR_ZERO(ENABLE_AUTO_POSITION)},
128 {NULL, 0}
129};
130
131static const struct ls_RegConst win_console_out_flags[] = {
132 // Console Output Flags
133 {"COF_PROCESSED_OUTPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_PROCESSED_OUTPUT)},
134 {"COF_WRAP_AT_EOL_OUTPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_WRAP_AT_EOL_OUTPUT)},
135 {"COF_VIRTUAL_TERMINAL_PROCESSING", CHECK_WIN_FLAG_OR_ZERO(ENABLE_VIRTUAL_TERMINAL_PROCESSING)},
136 {"COF_DISABLE_NEWLINE_AUTO_RETURN", CHECK_WIN_FLAG_OR_ZERO(DISABLE_NEWLINE_AUTO_RETURN)},
137 {"COF_ENABLE_LVB_GRID_WORLDWIDE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_LVB_GRID_WORLDWIDE)},
138 {NULL, 0}
139};
140
141
142// Export Unix constants to Lua
143static const struct ls_RegConst nix_tcsetattr_actions[] = {
144 // The optional actions for tcsetattr
145 {"TCSANOW", CHECK_NIX_FLAG_OR_ZERO(TCSANOW)},
146 {"TCSADRAIN", CHECK_NIX_FLAG_OR_ZERO(TCSADRAIN)},
147 {"TCSAFLUSH", CHECK_NIX_FLAG_OR_ZERO(TCSAFLUSH)},
148 {NULL, 0}
149};
150
151static const struct ls_RegConst nix_console_i_flags[] = {
152 // Input flags (c_iflag)
153 {"I_IGNBRK", CHECK_NIX_FLAG_OR_ZERO(IGNBRK)},
154 {"I_BRKINT", CHECK_NIX_FLAG_OR_ZERO(BRKINT)},
155 {"I_IGNPAR", CHECK_NIX_FLAG_OR_ZERO(IGNPAR)},
156 {"I_PARMRK", CHECK_NIX_FLAG_OR_ZERO(PARMRK)},
157 {"I_INPCK", CHECK_NIX_FLAG_OR_ZERO(INPCK)},
158 {"I_ISTRIP", CHECK_NIX_FLAG_OR_ZERO(ISTRIP)},
159 {"I_INLCR", CHECK_NIX_FLAG_OR_ZERO(INLCR)},
160 {"I_IGNCR", CHECK_NIX_FLAG_OR_ZERO(IGNCR)},
161 {"I_ICRNL", CHECK_NIX_FLAG_OR_ZERO(ICRNL)},
162#ifndef __APPLE__
163 {"I_IUCLC", CHECK_NIX_FLAG_OR_ZERO(IUCLC)}, // Might not be available on all systems
164#else
165 {"I_IUCLC", 0},
166#endif
167 {"I_IXON", CHECK_NIX_FLAG_OR_ZERO(IXON)},
168 {"I_IXANY", CHECK_NIX_FLAG_OR_ZERO(IXANY)},
169 {"I_IXOFF", CHECK_NIX_FLAG_OR_ZERO(IXOFF)},
170 {"I_IMAXBEL", CHECK_NIX_FLAG_OR_ZERO(IMAXBEL)},
171 {NULL, 0}
30}; 172};
31 173
174static const struct ls_RegConst nix_console_o_flags[] = {
175 // Output flags (c_oflag)
176 {"O_OPOST", CHECK_NIX_FLAG_OR_ZERO(OPOST)},
177#ifndef __APPLE__
178 {"O_OLCUC", CHECK_NIX_FLAG_OR_ZERO(OLCUC)}, // Might not be available on all systems
179#else
180 {"O_OLCUC", 0},
181#endif
182 {"O_ONLCR", CHECK_NIX_FLAG_OR_ZERO(ONLCR)},
183 {"O_OCRNL", CHECK_NIX_FLAG_OR_ZERO(OCRNL)},
184 {"O_ONOCR", CHECK_NIX_FLAG_OR_ZERO(ONOCR)},
185 {"O_ONLRET", CHECK_NIX_FLAG_OR_ZERO(ONLRET)},
186 {"O_OFILL", CHECK_NIX_FLAG_OR_ZERO(OFILL)},
187 {"O_OFDEL", CHECK_NIX_FLAG_OR_ZERO(OFDEL)},
188 {"O_NLDLY", CHECK_NIX_FLAG_OR_ZERO(NLDLY)},
189 {"O_CRDLY", CHECK_NIX_FLAG_OR_ZERO(CRDLY)},
190 {"O_TABDLY", CHECK_NIX_FLAG_OR_ZERO(TABDLY)},
191 {"O_BSDLY", CHECK_NIX_FLAG_OR_ZERO(BSDLY)},
192 {"O_VTDLY", CHECK_NIX_FLAG_OR_ZERO(VTDLY)},
193 {"O_FFDLY", CHECK_NIX_FLAG_OR_ZERO(FFDLY)},
194 {NULL, 0}
195};
196
197static const struct ls_RegConst nix_console_l_flags[] = {
198 // Local flags (c_lflag)
199 {"L_ISIG", CHECK_NIX_FLAG_OR_ZERO(ISIG)},
200 {"L_ICANON", CHECK_NIX_FLAG_OR_ZERO(ICANON)},
201#ifndef __APPLE__
202 {"L_XCASE", CHECK_NIX_FLAG_OR_ZERO(XCASE)}, // Might not be available on all systems
203#else
204 {"L_XCASE", 0},
205#endif
206 {"L_ECHO", CHECK_NIX_FLAG_OR_ZERO(ECHO)},
207 {"L_ECHOE", CHECK_NIX_FLAG_OR_ZERO(ECHOE)},
208 {"L_ECHOK", CHECK_NIX_FLAG_OR_ZERO(ECHOK)},
209 {"L_ECHONL", CHECK_NIX_FLAG_OR_ZERO(ECHONL)},
210 {"L_NOFLSH", CHECK_NIX_FLAG_OR_ZERO(NOFLSH)},
211 {"L_TOSTOP", CHECK_NIX_FLAG_OR_ZERO(TOSTOP)},
212 {"L_ECHOCTL", CHECK_NIX_FLAG_OR_ZERO(ECHOCTL)}, // Might not be available on all systems
213 {"L_ECHOPRT", CHECK_NIX_FLAG_OR_ZERO(ECHOPRT)}, // Might not be available on all systems
214 {"L_ECHOKE", CHECK_NIX_FLAG_OR_ZERO(ECHOKE)}, // Might not be available on all systems
215 {"L_FLUSHO", CHECK_NIX_FLAG_OR_ZERO(FLUSHO)},
216 {"L_PENDIN", CHECK_NIX_FLAG_OR_ZERO(PENDIN)},
217 {"L_IEXTEN", CHECK_NIX_FLAG_OR_ZERO(IEXTEN)},
218 {NULL, 0}
219};
220
221static DWORD win_valid_in_flags = 0;
222static DWORD win_valid_out_flags = 0;
223static DWORD nix_valid_i_flags = 0;
224static DWORD nix_valid_o_flags = 0;
225static DWORD nix_valid_l_flags = 0;
226static void initialize_valid_flags()
227{
228 win_valid_in_flags = 0;
229 for (int i = 0; win_console_in_flags[i].name != NULL; i++)
230 {
231 win_valid_in_flags |= win_console_in_flags[i].value;
232 }
233 win_valid_out_flags = 0;
234 for (int i = 0; win_console_out_flags[i].name != NULL; i++)
235 {
236 win_valid_out_flags |= win_console_out_flags[i].value;
237 }
238 nix_valid_i_flags = 0;
239 for (int i = 0; nix_console_i_flags[i].name != NULL; i++)
240 {
241 nix_valid_i_flags |= nix_console_i_flags[i].value;
242 }
243 nix_valid_o_flags = 0;
244 for (int i = 0; nix_console_o_flags[i].name != NULL; i++)
245 {
246 nix_valid_o_flags |= nix_console_o_flags[i].value;
247 }
248 nix_valid_l_flags = 0;
249 for (int i = 0; nix_console_l_flags[i].name != NULL; i++)
250 {
251 nix_valid_l_flags |= nix_console_l_flags[i].value;
252 }
253}
254
255#ifdef _WIN32
256// first item on the stack should be io.stdin, io.stderr, or io.stdout, second item
257// should be the flags to validate.
258// If it returns NULL, then it leaves nil+err on the stack
259static HANDLE get_console_handle(lua_State *L, int flags_optional)
260{
261 if (lua_gettop(L) < 1) {
262 luaL_argerror(L, 1, "expected file handle");
263 }
264
265 HANDLE handle;
266 DWORD valid;
267 FILE *file = *(FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE);
268 if (file == stdin && file != NULL) {
269 handle = GetStdHandle(STD_INPUT_HANDLE);
270 valid = win_valid_in_flags;
271
272 } else if (file == stdout && file != NULL) {
273 handle = GetStdHandle(STD_OUTPUT_HANDLE);
274 valid = win_valid_out_flags;
275
276 } else if (file == stderr && file != NULL) {
277 handle = GetStdHandle(STD_ERROR_HANDLE);
278 valid = win_valid_out_flags;
279
280 } else {
281 luaL_argerror(L, 1, "invalid file handle"); // does not return
282 }
283
284 if (handle == INVALID_HANDLE_VALUE) {
285 termFormatError(L, GetLastError(), "failed to retrieve std handle");
286 lua_error(L); // does not return
287 }
288
289 if (handle == NULL) {
290 lua_pushnil(L);
291 lua_pushliteral(L, "failed to get console handle");
292 return NULL;
293 }
294
295 if (flags_optional && lua_gettop(L) < 2) {
296 return handle;
297 }
298
299 if (lua_gettop(L) < 2) {
300 luaL_argerror(L, 2, "expected flags");
301 }
302
303 LSBF_BITFLAG flags = lsbf_checkbitflags(L, 2);
304 if ((flags & ~valid) != 0) {
305 luaL_argerror(L, 2, "invalid flags");
306 }
307
308 return handle;
309}
310#else
311// first item on the stack should be io.stdin, io.stderr, or io.stdout. Throws a
312// Lua error if the file is not one of these.
313static int get_console_handle(lua_State *L)
314{
315 FILE **file = (FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE);
316 if (file == NULL || *file == NULL) {
317 return luaL_argerror(L, 1, "expected file handle"); // call doesn't return
318 }
319
320 // Check if the file is stdin, stdout, or stderr
321 if (*file == stdin || *file == stdout || *file == stderr) {
322 // Push the file descriptor onto the Lua stack
323 return fileno(*file);
324 }
325
326 return luaL_argerror(L, 1, "invalid file handle"); // does not return
327}
328#endif
329
330
331
332/***
333Sets the console flags (Windows).
334The `CIF_` and `COF_` constants are available on the module table. Where `CIF` are the
335input flags (for use with `io.stdin`) and `COF` are the output flags (for use with
336`io.stdout`/`io.stderr`).
337
338To see flag status and constant names check `listconsoleflags`.
339
340Note: not all combinations of flags are allowed, as some are mutually exclusive or mutually required.
341See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
342@function setconsoleflags
343@tparam file file file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
344@tparam bitflags bitflags the flags to set/unset
345@treturn[1] boolean `true` on success
346@treturn[2] nil
347@treturn[2] string error message
348@usage
349local system = require('system')
350system.listconsoleflags(io.stdout) -- List all the available flags and their current status
351
352local flags = system.getconsoleflags(io.stdout)
353assert(system.setconsoleflags(io.stdout,
354 flags + system.COF_VIRTUAL_TERMINAL_PROCESSING)
355
356system.listconsoleflags(io.stdout) -- List again to check the differences
357*/
358static int lst_setconsoleflags(lua_State *L)
359{
360#ifdef _WIN32
361 HANDLE console_handle = get_console_handle(L, 0);
362 if (console_handle == NULL) {
363 return 2; // error message is already on the stack
364 }
365 LSBF_BITFLAG new_console_mode = lsbf_checkbitflags(L, 2);
366
367 if (!SetConsoleMode(console_handle, new_console_mode)) {
368 termFormatError(L, GetLastError(), "failed to set console mode");
369 return 2;
370 }
371
372#else
373 get_console_handle(L); // to validate args
374#endif
375
376 lua_pushboolean(L, 1);
377 return 1;
378}
379
380
381
382/***
383Gets console flags (Windows).
384The `CIF_` and `COF_` constants are available on the module table. Where `CIF` are the
385input flags (for use with `io.stdin`) and `COF` are the output flags (for use with
386`io.stdout`/`io.stderr`).
387
388_Note_: See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
389for more information on the flags.
390
391
392
393@function getconsoleflags
394@tparam file file file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
395@treturn[1] bitflags the current console flags.
396@treturn[2] nil
397@treturn[2] string error message
398@usage
399local system = require('system')
400
401local flags = system.getconsoleflags(io.stdout)
402print("Current stdout flags:", tostring(flags))
403
404if flags:has_all_of(system.COF_VIRTUAL_TERMINAL_PROCESSING + system.COF_PROCESSED_OUTPUT) then
405 print("Both flags are set")
406else
407 print("At least one flag is not set")
408end
409*/
410static int lst_getconsoleflags(lua_State *L)
411{
412 DWORD console_mode = 0;
413
414#ifdef _WIN32
415 HANDLE console_handle = get_console_handle(L, 1);
416 if (console_handle == NULL) {
417 return 2; // error message is already on the stack
418 }
419
420 if (GetConsoleMode(console_handle, &console_mode) == 0)
421 {
422 lua_pushnil(L);
423 lua_pushliteral(L, "failed to get console mode");
424 return 2;
425 }
426#else
427 get_console_handle(L); // to validate args
428
429#endif
430 lsbf_pushbitflags(L, console_mode);
431 return 1;
432}
433
434
435
436/*-------------------------------------------------------------------------
437 * Unix tcgetattr/tcsetattr functions
438 *-------------------------------------------------------------------------*/
439// Code modified from the LuaPosix library by Gary V. Vaughan
440// see https://github.com/luaposix/luaposix
441
442/***
443Get termios state (Posix).
444The terminal attributes is a table with the following fields:
445
446- `iflag` input flags
447- `oflag` output flags
448- `lflag` local flags
449- `cflag` control flags
450- `ispeed` input speed
451- `ospeed` output speed
452- `cc` control characters
453
454@function tcgetattr
455@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
456@treturn[1] termios terminal attributes, if successful. On Windows the bitflags are all 0, and the `cc` table is empty.
457@treturn[2] nil
458@treturn[2] string error message
459@treturn[2] int errnum
460@return error message if failed
461@usage
462local system = require('system')
463
464local status = assert(tcgetattr(io.stdin))
465if status.iflag:has_all_of(system.I_IGNBRK) then
466 print("Ignoring break condition")
467end
468*/
469static int lst_tcgetattr(lua_State *L)
470{
471#ifndef _WIN32
472 int r, i;
473 struct termios t;
474 int fd = get_console_handle(L);
475
476 r = tcgetattr(fd, &t);
477 if (r == -1) return pusherror(L, NULL);
478
479 lua_newtable(L);
480 lsbf_pushbitflags(L, t.c_iflag);
481 lua_setfield(L, -2, "iflag");
482
483 lsbf_pushbitflags(L, t.c_oflag);
484 lua_setfield(L, -2, "oflag");
485
486 lsbf_pushbitflags(L, t.c_lflag);
487 lua_setfield(L, -2, "lflag");
488
489 lsbf_pushbitflags(L, t.c_cflag);
490 lua_setfield(L, -2, "cflag");
491
492 lua_pushinteger(L, cfgetispeed(&t));
493 lua_setfield(L, -2, "ispeed");
494
495 lua_pushinteger(L, cfgetospeed(&t));
496 lua_setfield(L, -2, "ospeed");
497
498 lua_newtable(L);
499 for (i=0; i<NCCS; i++)
500 {
501 lua_pushinteger(L, i);
502 lua_pushinteger(L, t.c_cc[i]);
503 lua_settable(L, -3);
504 }
505 lua_setfield(L, -2, "cc");
506
507#else
508 lua_settop(L, 1); // remove all but file handle
509 get_console_handle(L, 1); //check args
510
511 lua_newtable(L);
512 lsbf_pushbitflags(L, 0);
513 lua_setfield(L, -2, "iflag");
514 lsbf_pushbitflags(L, 0);
515 lua_setfield(L, -2, "oflag");
516 lsbf_pushbitflags(L, 0);
517 lua_setfield(L, -2, "lflag");
518 lsbf_pushbitflags(L, 0);
519 lua_setfield(L, -2, "cflag");
520 lua_pushinteger(L, 0);
521 lua_setfield(L, -2, "ispeed");
522 lua_pushinteger(L, 0);
523 lua_setfield(L, -2, "ospeed");
524 lua_newtable(L);
525 lua_setfield(L, -2, "cc");
526
527#endif
528 return 1;
529}
530
531
532
533/***
534Set termios state (Posix).
535This function will set the flags as given.
536
537The `I_`, `O_`, and `L_` constants are available on the module table. They are the respective
538flags for the `iflags`, `oflags`, and `lflags` bitmasks.
539
540To see flag status and constant names check `listtermflags`. For their meaning check
541[the manpage](https://www.man7.org/linux/man-pages/man3/termios.3.html).
542
543_Note_: only `iflag`, `oflag`, and `lflag` are supported at the moment. The other fields are ignored.
544@function tcsetattr
545@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
546@int actions one of `TCSANOW`, `TCSADRAIN`, `TCSAFLUSH`
547@tparam table termios a table with bitflag fields:
548@tparam[opt] bitflags termios.iflag if given will set the input flags
549@tparam[opt] bitflags termios.oflag if given will set the output flags
550@tparam[opt] bitflags termios.lflag if given will set the local flags
551@treturn[1] bool `true`, if successful. Always returns `true` on Windows.
552@return[2] nil
553@treturn[2] string error message
554@treturn[2] int errnum
555@usage
556local system = require('system')
557
558local status = assert(tcgetattr(io.stdin))
559if not status.lflag:has_all_of(system.L_ECHO) then
560 -- if echo is off, turn echoing newlines on
561 tcsetattr(io.stdin, system.TCSANOW, { lflag = status.lflag + system.L_ECHONL }))
562end
563*/
564static int lst_tcsetattr(lua_State *L)
565{
566#ifndef _WIN32
567 struct termios t;
568 int r, i;
569 int fd = get_console_handle(L); // first is the console handle
570 int act = luaL_checkinteger(L, 2); // second is the action to take
571
572 r = tcgetattr(fd, &t);
573 if (r == -1) return pusherror(L, NULL);
574
575 t.c_iflag = lsbf_checkbitflagsfield(L, 3, "iflag", t.c_iflag);
576 t.c_oflag = lsbf_checkbitflagsfield(L, 3, "oflag", t.c_oflag);
577 t.c_lflag = lsbf_checkbitflagsfield(L, 3, "lflag", t.c_lflag);
578
579 // Skipping the others for now
580
581 // lua_getfield(L, 3, "cflag"); t.c_cflag = optint(L, -1, 0); lua_pop(L, 1);
582 // lua_getfield(L, 3, "ispeed"); cfsetispeed( &t, optint(L, -1, B0) ); lua_pop(L, 1);
583 // lua_getfield(L, 3, "ospeed"); cfsetospeed( &t, optint(L, -1, B0) ); lua_pop(L, 1);
584
585 // lua_getfield(L, 3, "cc");
586 // for (i=0; i<NCCS; i++)
587 // {
588 // lua_pushinteger(L, i);
589 // lua_gettable(L, -2);
590 // t.c_cc[i] = optint(L, -1, 0);
591 // lua_pop(L, 1);
592 // }
593
594 r = tcsetattr(fd, act, &t);
595 if (r == -1) return pusherror(L, NULL);
596
597#else
598 // Windows does not have a tcsetattr function, but we check arguments anyway
599 luaL_checkinteger(L, 2);
600 lsbf_checkbitflagsfield(L, 3, "iflag", 0);
601 lsbf_checkbitflagsfield(L, 3, "oflag", 0);
602 lsbf_checkbitflagsfield(L, 3, "lflag", 0);
603 lua_settop(L, 1); // remove all but file handle
604 get_console_handle(L, 1);
605#endif
606
607 lua_pushboolean(L, 1);
608 return 1;
609}
610
611
612
613/***
614Enables or disables non-blocking mode for a file (Posix).
615@function setnonblock
616@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
617@tparam boolean make_non_block a truthy value will enable non-blocking mode, a falsy value will disable it.
618@treturn[1] bool `true`, if successful
619@treturn[2] nil
620@treturn[2] string error message
621@treturn[2] int errnum
622@see getnonblock
623@usage
624local sys = require('system')
625
626-- set io.stdin to non-blocking mode
627local old_setting = sys.getnonblock(io.stdin)
628sys.setnonblock(io.stdin, true)
629
630-- do stuff
631
632-- restore old setting
633sys.setnonblock(io.stdin, old_setting)
634*/
635static int lst_setnonblock(lua_State *L)
636{
637#ifndef _WIN32
638
639 int fd = get_console_handle(L);
640
641 int flags = fcntl(fd, F_GETFL, 0);
642 if (flags == -1) {
643 return pusherror(L, "Error getting handle flags: ");
644 }
645 if (lua_toboolean(L, 2)) {
646 // truthy: set non-blocking
647 flags |= O_NONBLOCK;
648 } else {
649 // falsy: set disable non-blocking
650 flags &= ~O_NONBLOCK;
651 }
652 if (fcntl(fd, F_SETFL, flags) == -1) {
653 return pusherror(L, "Error changing O_NONBLOCK: ");
654 }
655
656#else
657 if (lua_gettop(L) > 1) {
658 lua_settop(L, 1); // use one argument, because the second boolean will fail as get_console_flags expects bitflags
659 }
660 HANDLE console_handle = get_console_handle(L, 1);
661 if (console_handle == NULL) {
662 return 2; // error message is already on the stack
663 }
664
665#endif
666
667 lua_pushboolean(L, 1);
668 return 1;
669}
670
671
672
673/***
674Gets non-blocking mode status for a file (Posix).
675@function getnonblock
676@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
677@treturn[1] bool `true` if set to non-blocking, `false` if not. Always returns `false` on Windows.
678@treturn[2] nil
679@treturn[2] string error message
680@treturn[2] int errnum
681*/
682static int lst_getnonblock(lua_State *L)
683{
684#ifndef _WIN32
685
686 int fd = get_console_handle(L);
687
688 // Set O_NONBLOCK
689 int flags = fcntl(fd, F_GETFL, 0);
690 if (flags == -1) {
691 return pusherror(L, "Error getting handle flags: ");
692 }
693 if (flags & O_NONBLOCK) {
694 lua_pushboolean(L, 1);
695 } else {
696 lua_pushboolean(L, 0);
697 }
698
699#else
700 if (lua_gettop(L) > 1) {
701 lua_settop(L, 1); // use one argument, because the second boolean will fail as get_console_flags expects bitflags
702 }
703 HANDLE console_handle = get_console_handle(L, 1);
704 if (console_handle == NULL) {
705 return 2; // error message is already on the stack
706 }
707
708 lua_pushboolean(L, 0);
709
710#endif
711 return 1;
712}
713
714
715
716/*-------------------------------------------------------------------------
717 * Reading keyboard input
718 *-------------------------------------------------------------------------*/
719
720#ifdef _WIN32
721// Define a static buffer for UTF-8 characters
722static char utf8_buffer[4];
723static int utf8_buffer_len = 0;
724static int utf8_buffer_index = 0;
725#endif
726
727
728/***
729Reads a key from the console non-blocking. This function should not be called
730directly, but through the `system.readkey` or `system.readansi` functions. It
731will return the next byte from the input stream, or `nil` if no key was pressed.
732
733On Posix, `io.stdin` must be set to non-blocking mode using `setnonblock`
734and canonical mode must be turned off using `tcsetattr`,
735before calling this function. Otherwise it will block. No conversions are
736done on Posix, so the byte read is returned as-is.
737
738On Windows this reads a wide character and converts it to UTF-8. Multi-byte
739sequences will be buffered internally and returned one byte at a time.
740
741@function _readkey
742@treturn[1] integer the byte read from the input stream
743@treturn[2] nil if no key was pressed
744@treturn[3] nil on error
745@treturn[3] string error message
746@treturn[3] int errnum (on posix)
747*/
748static int lst_readkey(lua_State *L) {
749#ifdef _WIN32
750 if (utf8_buffer_len > 0) {
751 // Buffer not empty, return the next byte
752 lua_pushinteger(L, (unsigned char)utf8_buffer[utf8_buffer_index]);
753 utf8_buffer_index++;
754 utf8_buffer_len--;
755 // printf("returning from buffer: %d\n", luaL_checkinteger(L, -1));
756 if (utf8_buffer_len == 0) {
757 utf8_buffer_index = 0;
758 }
759 return 1;
760 }
761
762 if (!_kbhit()) {
763 return 0;
764 }
765
766 wchar_t wc = _getwch();
767 // printf("----\nread wchar_t: %x\n", wc);
768 if (wc == WEOF) {
769 lua_pushnil(L);
770 lua_pushliteral(L, "read error");
771 return 2;
772 }
773
774 if (sizeof(wchar_t) == 2) {
775 // printf("2-byte wchar_t\n");
776 // only 2 bytes wide, not 4
777 if (wc >= 0xD800 && wc <= 0xDBFF) {
778 // printf("2-byte wchar_t, received high, getting low...\n");
779
780 // we got a high surrogate, so we need to read the next one as the low surrogate
781 if (!_kbhit()) {
782 lua_pushnil(L);
783 lua_pushliteral(L, "incomplete surrogate pair");
784 return 2;
785 }
786
787 wchar_t wc2 = _getwch();
788 // printf("read wchar_t 2: %x\n", wc2);
789 if (wc2 == WEOF) {
790 lua_pushnil(L);
791 lua_pushliteral(L, "read error");
792 return 2;
793 }
794
795 if (wc2 < 0xDC00 || wc2 > 0xDFFF) {
796 lua_pushnil(L);
797 lua_pushliteral(L, "invalid surrogate pair");
798 return 2;
799 }
800 // printf("2-byte pair complete now\n");
801 wchar_t wch_pair[2] = { wc, wc2 };
802 utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, wch_pair, 2, utf8_buffer, sizeof(utf8_buffer), NULL, NULL);
803
804 } else {
805 // printf("2-byte wchar_t, no surrogate pair\n");
806 // not a high surrogate, so we can handle just the 2 bytes directly
807 utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8_buffer, sizeof(utf8_buffer), NULL, NULL);
808 }
809
810 } else {
811 // printf("4-byte wchar_t\n");
812 // 4 bytes wide, so handle as UTF-32 directly
813 utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8_buffer, sizeof(utf8_buffer), NULL, NULL);
814 }
815 // printf("utf8_buffer_len: %d\n", utf8_buffer_len);
816 utf8_buffer_index = 0;
817 if (utf8_buffer_len <= 0) {
818 lua_pushnil(L);
819 lua_pushliteral(L, "UTF-8 conversion error");
820 return 2;
821 }
822
823 lua_pushinteger(L, (unsigned char)utf8_buffer[utf8_buffer_index]);
824 utf8_buffer_index++;
825 utf8_buffer_len--;
826 // printf("returning from buffer: %x\n", luaL_checkinteger(L, -1));
827 return 1;
828
829#else
830 // Posix implementation
831 char ch;
832 ssize_t bytes_read = read(STDIN_FILENO, &ch, 1);
833 if (bytes_read > 0) {
834 lua_pushinteger(L, (unsigned char)ch);
835 return 1;
836
837 } else if (bytes_read == 0) {
838 return 0; // End of file or stream closed
839
840 } else {
841 if (errno == EAGAIN || errno == EWOULDBLOCK) {
842 // Resource temporarily unavailable, no data available to read
843 return 0;
844 } else {
845 return pusherror(L, "read error");
846 }
847 }
848
849#endif
850}
851
852
853
854/*-------------------------------------------------------------------------
855 * Retrieve terminal size
856 *-------------------------------------------------------------------------*/
857
858
859/***
860Get the size of the terminal in rows and columns.
861@function termsize
862@treturn[1] int the number of rows
863@treturn[1] int the number of columns
864@treturn[2] nil
865@treturn[2] string error message
866*/
867static int lst_termsize(lua_State *L) {
868 int columns, rows;
869
870#ifdef _WIN32
871 CONSOLE_SCREEN_BUFFER_INFO csbi;
872 if (!GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
873 termFormatError(L, GetLastError(), "Failed to get terminal size.");
874 return 2;
875 }
876 columns = csbi.srWindow.Right - csbi.srWindow.Left + 1;
877 rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
878
879#else
880 struct winsize ws;
881 if (ioctl(1, TIOCGWINSZ, &ws) == -1) {
882 return pusherror(L, "Failed to get terminal size.");
883 }
884 columns = ws.ws_col;
885 rows = ws.ws_row;
886
887#endif
888 lua_pushinteger(L, rows);
889 lua_pushinteger(L, columns);
890 return 2;
891}
892
893
894
895/*-------------------------------------------------------------------------
896 * utf8 conversion and support
897 *-------------------------------------------------------------------------*/
898
899// Function to convert a single UTF-8 character to a Unicode code point (uint32_t)
900// To prevent having to do codepage/locale changes, we use a custom implementation
901int utf8_to_wchar(const char *utf8, size_t len, mk_wchar_t *codepoint) {
902 if (len == 0) {
903 return -1; // No input provided
904 }
905
906 unsigned char c = (unsigned char)utf8[0];
907 if (c <= 0x7F) {
908 *codepoint = c;
909 return 1;
910 } else if ((c & 0xE0) == 0xC0) {
911 if (len < 2) return -1; // Not enough bytes
912 *codepoint = ((utf8[0] & 0x1F) << 6) | (utf8[1] & 0x3F);
913 return 2;
914 } else if ((c & 0xF0) == 0xE0) {
915 if (len < 3) return -1; // Not enough bytes
916 *codepoint = ((utf8[0] & 0x0F) << 12) | ((utf8[1] & 0x3F) << 6) | (utf8[2] & 0x3F);
917 return 3;
918 } else if ((c & 0xF8) == 0xF0) {
919 if (len < 4) return -1; // Not enough bytes
920 *codepoint = ((utf8[0] & 0x07) << 18) | ((utf8[1] & 0x3F) << 12) | ((utf8[2] & 0x3F) << 6) | (utf8[3] & 0x3F);
921 return 4;
922 } else {
923 // Invalid UTF-8 character
924 return -1;
925 }
926}
927
928
929/***
930Get the width of a utf8 character for terminal display.
931@function utf8cwidth
932@tparam string utf8_char the utf8 character to check, only the width of the first character will be returned
933@treturn[1] int the display width in columns of the first character in the string (0 for an empty string)
934@treturn[2] nil
935@treturn[2] string error message
936*/
937int lst_utf8cwidth(lua_State *L) {
938 const char *utf8_char;
939 size_t utf8_len;
940 utf8_char = luaL_checklstring(L, 1, &utf8_len);
941 int width = 0;
942
943 mk_wchar_t wc;
944
945 if (utf8_len == 0) {
946 lua_pushinteger(L, 0);
947 return 1;
948 }
949
950 // Convert the UTF-8 string to a wide character
951 int bytes_processed = utf8_to_wchar(utf8_char, utf8_len, &wc);
952 if (bytes_processed == -1) {
953 lua_pushnil(L);
954 lua_pushstring(L, "Invalid UTF-8 character");
955 return 2;
956 }
957
958 // Get the width of the wide character
959 width = mk_wcwidth(wc);
960 if (width == -1) {
961 lua_pushnil(L);
962 lua_pushstring(L, "Character width determination failed");
963 return 2;
964 }
965
966 lua_pushinteger(L, width);
967 return 1;
968}
969
970
971
972
973/***
974Get the width of a utf8 string for terminal display.
975@function utf8swidth
976@tparam string utf8_string the utf8 string to check
977@treturn[1] int the display width of the string in columns (0 for an empty string)
978@treturn[2] nil
979@treturn[2] string error message
980*/
981int lst_utf8swidth(lua_State *L) {
982 const char *utf8_str;
983 size_t utf8_len;
984 utf8_str = luaL_checklstring(L, 1, &utf8_len);
985 int total_width = 0;
986
987 if (utf8_len == 0) {
988 lua_pushinteger(L, 0);
989 return 1;
990 }
991
992 int bytes_processed = 0;
993 size_t i = 0;
994 mk_wchar_t wc;
995
996 while (i < utf8_len) {
997 bytes_processed = utf8_to_wchar(utf8_str + i, utf8_len - i, &wc);
998 if (bytes_processed == -1) {
999 lua_pushnil(L);
1000 lua_pushstring(L, "Invalid UTF-8 character");
1001 return 2;
1002 }
1003
1004 int width = mk_wcwidth(wc);
1005 if (width == -1) {
1006 lua_pushnil(L);
1007 lua_pushstring(L, "Character width determination failed");
1008 return 2;
1009 }
1010
1011 total_width += width;
1012 i += bytes_processed;
1013 }
1014
1015 lua_pushinteger(L, total_width);
1016 return 1;
1017}
1018
1019
1020
1021/*-------------------------------------------------------------------------
1022 * Windows codepage functions
1023 *-------------------------------------------------------------------------*/
1024
1025
1026/***
1027Gets the current console code page (Windows).
1028@function getconsolecp
1029@treturn[1] int the current code page (always 65001 on Posix systems)
1030*/
1031static int lst_getconsolecp(lua_State *L) {
1032 unsigned int cp = 65001;
1033#ifdef _WIN32
1034 cp = GetConsoleCP();
1035#endif
1036 lua_pushinteger(L, cp);
1037 return 1;
1038}
1039
1040
1041
1042/***
1043Sets the current console code page (Windows).
1044@function setconsolecp
1045@tparam int cp the code page to set, use `system.CODEPAGE_UTF8` (65001) for UTF-8
1046@treturn[1] bool `true` on success (always `true` on Posix systems)
1047*/
1048static int lst_setconsolecp(lua_State *L) {
1049 unsigned int cp = (unsigned int)luaL_checkinteger(L, 1);
1050 int success = TRUE;
1051#ifdef _WIN32
1052 SetConsoleCP(cp);
1053#endif
1054 lua_pushboolean(L, success);
1055 return 1;
1056}
1057
1058
1059
1060/***
1061Gets the current console output code page (Windows).
1062@function getconsoleoutputcp
1063@treturn[1] int the current code page (always 65001 on Posix systems)
1064*/
1065static int lst_getconsoleoutputcp(lua_State *L) {
1066 unsigned int cp = 65001;
1067#ifdef _WIN32
1068 cp = GetConsoleOutputCP();
1069#endif
1070 lua_pushinteger(L, cp);
1071 return 1;
1072}
1073
1074
1075
1076/***
1077Sets the current console output code page (Windows).
1078@function setconsoleoutputcp
1079@tparam int cp the code page to set, use `system.CODEPAGE_UTF8` (65001) for UTF-8
1080@treturn[1] bool `true` on success (always `true` on Posix systems)
1081*/
1082static int lst_setconsoleoutputcp(lua_State *L) {
1083 unsigned int cp = (unsigned int)luaL_checkinteger(L, 1);
1084 int success = TRUE;
1085#ifdef _WIN32
1086 SetConsoleOutputCP(cp);
1087#endif
1088 lua_pushboolean(L, success);
1089 return 1;
1090}
1091
1092
1093
32/*------------------------------------------------------------------------- 1094/*-------------------------------------------------------------------------
33 * Initializes module 1095 * Initializes module
34 *-------------------------------------------------------------------------*/ 1096 *-------------------------------------------------------------------------*/
1097
1098static luaL_Reg func[] = {
1099 { "isatty", lst_isatty },
1100 { "getconsoleflags", lst_getconsoleflags },
1101 { "setconsoleflags", lst_setconsoleflags },
1102 { "tcgetattr", lst_tcgetattr },
1103 { "tcsetattr", lst_tcsetattr },
1104 { "getnonblock", lst_getnonblock },
1105 { "setnonblock", lst_setnonblock },
1106 { "_readkey", lst_readkey },
1107 { "termsize", lst_termsize },
1108 { "utf8cwidth", lst_utf8cwidth },
1109 { "utf8swidth", lst_utf8swidth },
1110 { "getconsolecp", lst_getconsolecp },
1111 { "setconsolecp", lst_setconsolecp },
1112 { "getconsoleoutputcp", lst_getconsoleoutputcp },
1113 { "setconsoleoutputcp", lst_setconsoleoutputcp },
1114 { NULL, NULL }
1115};
1116
1117
1118
35void term_open(lua_State *L) { 1119void term_open(lua_State *L) {
1120 // set up constants and export the constants in module table
1121 initialize_valid_flags();
1122 // Windows flags
1123 for (int i = 0; win_console_in_flags[i].name != NULL; i++)
1124 {
1125 lsbf_pushbitflags(L, win_console_in_flags[i].value);
1126 lua_setfield(L, -2, win_console_in_flags[i].name);
1127 }
1128 for (int i = 0; win_console_out_flags[i].name != NULL; i++)
1129 {
1130 lsbf_pushbitflags(L, win_console_out_flags[i].value);
1131 lua_setfield(L, -2, win_console_out_flags[i].name);
1132 }
1133 // Unix flags
1134 for (int i = 0; nix_console_i_flags[i].name != NULL; i++)
1135 {
1136 lsbf_pushbitflags(L, nix_console_i_flags[i].value);
1137 lua_setfield(L, -2, nix_console_i_flags[i].name);
1138 }
1139 for (int i = 0; nix_console_o_flags[i].name != NULL; i++)
1140 {
1141 lsbf_pushbitflags(L, nix_console_o_flags[i].value);
1142 lua_setfield(L, -2, nix_console_o_flags[i].name);
1143 }
1144 for (int i = 0; nix_console_l_flags[i].name != NULL; i++)
1145 {
1146 lsbf_pushbitflags(L, nix_console_l_flags[i].value);
1147 lua_setfield(L, -2, nix_console_l_flags[i].name);
1148 }
1149 // Unix tcsetattr actions
1150 for (int i = 0; nix_tcsetattr_actions[i].name != NULL; i++)
1151 {
1152 lua_pushinteger(L, nix_tcsetattr_actions[i].value);
1153 lua_setfield(L, -2, nix_tcsetattr_actions[i].name);
1154 }
1155
1156 // export functions
36 luaL_setfuncs(L, func, 0); 1157 luaL_setfuncs(L, func, 0);
37} 1158}
diff --git a/src/time.c b/src/time.c
index 5f0ead0..05f4f1b 100644
--- a/src/time.c
+++ b/src/time.c
@@ -1,4 +1,8 @@
1/// @submodule system 1/// @module system
2
3/// Time.
4// @section time
5
2#include <lua.h> 6#include <lua.h>
3#include <lauxlib.h> 7#include <lauxlib.h>
4 8
diff --git a/src/wcwidth.c b/src/wcwidth.c
new file mode 100644
index 0000000..6032158
--- /dev/null
+++ b/src/wcwidth.c
@@ -0,0 +1,285 @@
1// This file was modified from the original versions, check "modified:" comments for details
2// Character range updates (both the table and the +1 check) were generated using ChatGPT.
3
4/*
5 * This is an implementation of wcwidth() and wcswidth() (defined in
6 * IEEE Std 1002.1-2001) for Unicode.
7 *
8 * http://www.opengroup.org/onlinepubs/007904975/functions/wcwidth.html
9 * http://www.opengroup.org/onlinepubs/007904975/functions/wcswidth.html
10 *
11 * In fixed-width output devices, Latin characters all occupy a single
12 * "cell" position of equal width, whereas ideographic CJK characters
13 * occupy two such cells. Interoperability between terminal-line
14 * applications and (teletype-style) character terminals using the
15 * UTF-8 encoding requires agreement on which character should advance
16 * the cursor by how many cell positions. No established formal
17 * standards exist at present on which Unicode character shall occupy
18 * how many cell positions on character terminals. These routines are
19 * a first attempt of defining such behavior based on simple rules
20 * applied to data provided by the Unicode Consortium.
21 *
22 * For some graphical characters, the Unicode standard explicitly
23 * defines a character-cell width via the definition of the East Asian
24 * FullWidth (F), Wide (W), Half-width (H), and Narrow (Na) classes.
25 * In all these cases, there is no ambiguity about which width a
26 * terminal shall use. For characters in the East Asian Ambiguous (A)
27 * class, the width choice depends purely on a preference of backward
28 * compatibility with either historic CJK or Western practice.
29 * Choosing single-width for these characters is easy to justify as
30 * the appropriate long-term solution, as the CJK practice of
31 * displaying these characters as double-width comes from historic
32 * implementation simplicity (8-bit encoded characters were displayed
33 * single-width and 16-bit ones double-width, even for Greek,
34 * Cyrillic, etc.) and not any typographic considerations.
35 *
36 * Much less clear is the choice of width for the Not East Asian
37 * (Neutral) class. Existing practice does not dictate a width for any
38 * of these characters. It would nevertheless make sense
39 * typographically to allocate two character cells to characters such
40 * as for instance EM SPACE or VOLUME INTEGRAL, which cannot be
41 * represented adequately with a single-width glyph. The following
42 * routines at present merely assign a single-cell width to all
43 * neutral characters, in the interest of simplicity. This is not
44 * entirely satisfactory and should be reconsidered before
45 * establishing a formal standard in this area. At the moment, the
46 * decision which Not East Asian (Neutral) characters should be
47 * represented by double-width glyphs cannot yet be answered by
48 * applying a simple rule from the Unicode database content. Setting
49 * up a proper standard for the behavior of UTF-8 character terminals
50 * will require a careful analysis not only of each Unicode character,
51 * but also of each presentation form, something the author of these
52 * routines has avoided to do so far.
53 *
54 * http://www.unicode.org/unicode/reports/tr11/
55 *
56 * Markus Kuhn -- 2007-05-26 (Unicode 5.0)
57 *
58 * Permission to use, copy, modify, and distribute this software
59 * for any purpose and without fee is hereby granted. The author
60 * disclaims all warranties with regard to this software.
61 *
62 * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
63 */
64
65#include "wcwidth.h" // modified: used to define mk_wchar_t
66
67struct interval {
68 int first;
69 int last;
70};
71
72/* auxiliary function for binary search in interval table */
73static int bisearch(mk_wchar_t ucs, const struct interval *table, int max) { // modified: use mk_wchar_t
74 int min = 0;
75 int mid;
76
77 if (ucs < table[0].first || ucs > table[max].last)
78 return 0;
79 while (max >= min) {
80 mid = (min + max) / 2;
81 if (ucs > table[mid].last)
82 min = mid + 1;
83 else if (ucs < table[mid].first)
84 max = mid - 1;
85 else
86 return 1;
87 }
88
89 return 0;
90}
91
92
93/* The following two functions define the column width of an ISO 10646
94 * character as follows:
95 *
96 * - The null character (U+0000) has a column width of 0.
97 *
98 * - Other C0/C1 control characters and DEL will lead to a return
99 * value of -1.
100 *
101 * - Non-spacing and enclosing combining characters (general
102 * category code Mn or Me in the Unicode database) have a
103 * column width of 0.
104 *
105 * - SOFT HYPHEN (U+00AD) has a column width of 1.
106 *
107 * - Other format characters (general category code Cf in the Unicode
108 * database) and ZERO WIDTH SPACE (U+200B) have a column width of 0.
109 *
110 * - Hangul Jamo medial vowels and final consonants (U+1160-U+11FF)
111 * have a column width of 0.
112 *
113 * - Spacing characters in the East Asian Wide (W) or East Asian
114 * Full-width (F) category as defined in Unicode Technical
115 * Report #11 have a column width of 2.
116 *
117 * - All remaining characters (including all printable
118 * ISO 8859-1 and WGL4 characters, Unicode control characters,
119 * etc.) have a column width of 1.
120 *
121 * This implementation assumes that mk_wchar_t characters are encoded
122 * in ISO 10646.
123 */
124
125int mk_wcwidth(mk_wchar_t ucs) // modified: use mk_wchar_t
126{
127 /* sorted list of non-overlapping intervals of non-spacing characters */
128 /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */
129 static const struct interval combining[] = { // modified: added new ranges to the list
130 { 0x0300, 0x036F }, { 0x0483, 0x0489 }, { 0x0591, 0x05BD },
131 { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, { 0x05C4, 0x05C5 },
132 { 0x05C7, 0x05C7 }, { 0x0600, 0x0605 }, { 0x0610, 0x061A },
133 { 0x061C, 0x061C }, { 0x064B, 0x065F }, { 0x0670, 0x0670 },
134 { 0x06D6, 0x06DC }, { 0x06DF, 0x06E4 }, { 0x06E7, 0x06E8 },
135 { 0x06EA, 0x06ED }, { 0x0711, 0x0711 }, { 0x0730, 0x074A },
136 { 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x07FD, 0x07FD },
137 { 0x0816, 0x0819 }, { 0x081B, 0x0823 }, { 0x0825, 0x0827 },
138 { 0x0829, 0x082D }, { 0x0859, 0x085B }, { 0x08D3, 0x08E1 },
139 { 0x08E3, 0x0903 }, { 0x093A, 0x093C }, { 0x093E, 0x094F },
140 { 0x0951, 0x0957 }, { 0x0962, 0x0963 }, { 0x0981, 0x0983 },
141 { 0x09BC, 0x09BC }, { 0x09BE, 0x09C4 }, { 0x09C7, 0x09C8 },
142 { 0x09CB, 0x09CD }, { 0x09D7, 0x09D7 }, { 0x09E2, 0x09E3 },
143 { 0x09FE, 0x09FE }, { 0x0A01, 0x0A03 }, { 0x0A3C, 0x0A3C },
144 { 0x0A3E, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D },
145 { 0x0A51, 0x0A51 }, { 0x0A70, 0x0A71 }, { 0x0A75, 0x0A75 },
146 { 0x0A81, 0x0A83 }, { 0x0ABC, 0x0ABC }, { 0x0ABE, 0x0AC5 },
147 { 0x0AC7, 0x0AC9 }, { 0x0ACB, 0x0ACD }, { 0x0AE2, 0x0AE3 },
148 { 0x0AFA, 0x0AFF }, { 0x0B01, 0x0B03 }, { 0x0B3C, 0x0B3C },
149 { 0x0B3E, 0x0B44 }, { 0x0B47, 0x0B48 }, { 0x0B4B, 0x0B4D },
150 { 0x0B55, 0x0B57 }, { 0x0B62, 0x0B63 }, { 0x0B82, 0x0B82 },
151 { 0x0BBE, 0x0BC2 }, { 0x0BC6, 0x0BC8 }, { 0x0BCA, 0x0BCD },
152 { 0x0BD7, 0x0BD7 }, { 0x0C00, 0x0C04 }, { 0x0C3E, 0x0C44 },
153 { 0x0C46, 0x0C48 }, { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 },
154 { 0x0C62, 0x0C63 }, { 0x0C81, 0x0C83 }, { 0x0CBC, 0x0CBC },
155 { 0x0CBE, 0x0CC4 }, { 0x0CC6, 0x0CC8 }, { 0x0CCA, 0x0CCD },
156 { 0x0CD5, 0x0CD6 }, { 0x0CE2, 0x0CE3 }, { 0x0D00, 0x0D03 },
157 { 0x0D3B, 0x0D3C }, { 0x0D3E, 0x0D44 }, { 0x0D46, 0x0D48 },
158 { 0x0D4A, 0x0D4D }, { 0x0D57, 0x0D57 }, { 0x0D62, 0x0D63 },
159 { 0x0D82, 0x0D83 }, { 0x0DCF, 0x0DD4 }, { 0x0DD6, 0x0DD6 },
160 { 0x0DD8, 0x0DDF }, { 0x0DF2, 0x0DF3 }, { 0x0E31, 0x0E31 },
161 { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, { 0x0EB1, 0x0EB1 },
162 { 0x0EB4, 0x0EBC }, { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 },
163 { 0x0F35, 0x0F35 }, { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 },
164 { 0x0F71, 0x0F7E }, { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 },
165 { 0x0F8D, 0x0F97 }, { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 },
166 { 0x102D, 0x1030 }, { 0x1032, 0x1037 }, { 0x1039, 0x103A },
167 { 0x103D, 0x103E }, { 0x1058, 0x1059 }, { 0x105E, 0x1060 },
168 { 0x1071, 0x1074 }, { 0x1082, 0x1082 }, { 0x1085, 0x1086 },
169 { 0x108D, 0x108D }, { 0x109D, 0x109D }, { 0x135D, 0x135F },
170 { 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 },
171 { 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD },
172 { 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD },
173 { 0x180B, 0x180E }, { 0x1885, 0x1886 }, { 0x18A9, 0x18A9 },
174 { 0x1920, 0x1922 }, { 0x1927, 0x1928 }, { 0x1932, 0x1932 },
175 { 0x1939, 0x193B }, { 0x1A17, 0x1A18 }, { 0x1A1B, 0x1A1B },
176 { 0x1A56, 0x1A56 }, { 0x1A58, 0x1A5E }, { 0x1A60, 0x1A60 },
177 { 0x1A62, 0x1A62 }, { 0x1A65, 0x1A6C }, { 0x1A73, 0x1A7C },
178 { 0x1A7F, 0x1A7F }, { 0x1AB0, 0x1ACE }, { 0x1B00, 0x1B03 },
179 { 0x1B34, 0x1B34 }, { 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C },
180 { 0x1B42, 0x1B42 }, { 0x1B6B, 0x1B73 }, { 0x1B80, 0x1B82 },
181 { 0x1BA1, 0x1BA1 }, { 0x1BA6, 0x1BA7 }, { 0x1BAA, 0x1BAA },
182 { 0x1BAB, 0x1BAD }, { 0x1BE6, 0x1BE6 }, { 0x1BE8, 0x1BE9 },
183 { 0x1BED, 0x1BED }, { 0x1BEF, 0x1BF1 }, { 0x1C2C, 0x1C33 },
184 { 0x1C36, 0x1C37 }, { 0x1CD0, 0x1CD2 }, { 0x1CD4, 0x1CE8 },
185 { 0x1CED, 0x1CED }, { 0x1CF4, 0x1CF4 }, { 0x1CF8, 0x1CF9 },
186 { 0x1DC0, 0x1DF9 }, { 0x1DFB, 0x1DFF }, { 0x20D0, 0x20DC },
187 { 0x20E1, 0x20E1 }, { 0x20E5, 0x20F0 }, { 0x2CEF, 0x2CF1 },
188 { 0x2D7F, 0x2D7F }, { 0x2DE0, 0x2DFF }, { 0x302A, 0x302D },
189 { 0x3099, 0x309A }, { 0xA66F, 0xA672 }, { 0xA674, 0xA67D },
190 { 0xA69E, 0xA69F }, { 0xA6F0, 0xA6F1 }, { 0xA802, 0xA802 },
191 { 0xA806, 0xA806 }, { 0xA80B, 0xA80B }, { 0xA825, 0xA826 },
192 { 0xA82C, 0xA82C }, { 0xA8C4, 0xA8C5 }, { 0xA8E0, 0xA8F1 },
193 { 0xA8FF, 0xA8FF }, { 0xA926, 0xA92D }, { 0xA947, 0xA951 },
194 { 0xA980, 0xA982 }, { 0xA9B3, 0xA9B3 }, { 0xA9B6, 0xA9B9 },
195 { 0xA9BC, 0xA9BD }, { 0xA9E5, 0xA9E5 }, { 0xAA29, 0xAA2E },
196 { 0xAA31, 0xAA32 }, { 0xAA35, 0xAA36 }, { 0xAA43, 0xAA43 },
197 { 0xAA4C, 0xAA4C }, { 0xAA7C, 0xAA7C }, { 0xAAB0, 0xAAB0 },
198 { 0xAAB2, 0xAAB4 }, { 0xAAB7, 0xAAB8 }, { 0xAABE, 0xAABF },
199 { 0xAAC1, 0xAAC1 }, { 0xAAEB, 0xAAEB }, { 0xAAEE, 0xAAEF },
200 { 0xAAF5, 0xAAF6 }, { 0xABE3, 0xABE4 }, { 0xABE6, 0xABE7 },
201 { 0xABE9, 0xABEA }, { 0xABEC, 0xABED }, { 0xFB1E, 0xFB1E },
202 { 0xFE00, 0xFE0F }, { 0xFE20, 0xFE2F }, { 0x101FD, 0x101FD },
203 { 0x102E0, 0x102E0 }, { 0x10376, 0x1037A }, { 0x10A01, 0x10A03 },
204 { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F }, { 0x10A38, 0x10A3A },
205 { 0x10A3F, 0x10A3F }, { 0x10AE5, 0x10AE6 }, { 0x10D24, 0x10D27 },
206 { 0x10EAB, 0x10EAC }, { 0x10F46, 0x10F50 }, { 0x10F82, 0x10F85 },
207 { 0x11000, 0x11002 }, { 0x11038, 0x11046 }, { 0x1107F, 0x11082 },
208 { 0x110B0, 0x110BA }, { 0x11100, 0x11102 }, { 0x11127, 0x11134 },
209 { 0x11145, 0x11146 }, { 0x11173, 0x11173 }, { 0x11180, 0x11182 },
210 { 0x111B3, 0x111C0 }, { 0x111C9, 0x111CC }, { 0x1122C, 0x11237 },
211 { 0x1123E, 0x1123E }, { 0x112DF, 0x112EA }, { 0x11300, 0x11303 },
212 { 0x1133B, 0x1133C }, { 0x1133E, 0x11344 }, { 0x11347, 0x11348 },
213 { 0x1134B, 0x1134D }, { 0x11357, 0x11357 }, { 0x11362, 0x11363 },
214 { 0x11435, 0x11446 }, { 0x1145E, 0x1145E }, { 0x114B0, 0x114C3 },
215 { 0x115AF, 0x115B5 }, { 0x115B8, 0x115C0 }, { 0x115DC, 0x115DD },
216 { 0x11630, 0x11640 }, { 0x116AB, 0x116B7 }, { 0x1171D, 0x1172B },
217 { 0x1182C, 0x1183A }, { 0x11930, 0x11935 }, { 0x11937, 0x11938 },
218 { 0x1193B, 0x1193E }, { 0x11940, 0x11940 }, { 0x11942, 0x11942 },
219 { 0x119D1, 0x119D7 }, { 0x119DA, 0x119E0 }, { 0x11A01, 0x11A0A },
220 { 0x11A33, 0x11A39 }, { 0x11A3B, 0x11A3E }, { 0x11A47, 0x11A47 },
221 { 0x11A51, 0x11A5B }, { 0x11A8A, 0x11A96 }, { 0x11A98, 0x11A99 },
222 { 0x11C30, 0x11C36 }, { 0x11C38, 0x11C3D }, { 0x11C3F, 0x11C3F },
223 { 0x11C92, 0x11CA7 }, { 0x11CAA, 0x11CB0 }, { 0x11CB2, 0x11CB3 },
224 { 0x11CB5, 0x11CB6 }, { 0x11D31, 0x11D36 }, { 0x11D3A, 0x11D3A },
225 { 0x11D3C, 0x11D3D }, { 0x11D3F, 0x11D45 }, { 0x11D47, 0x11D47 },
226 { 0x11D90, 0x11D91 }, { 0x11D95, 0x11D95 }, { 0x11D97, 0x11D97 },
227 { 0x11EF3, 0x11EF4 }, { 0x13430, 0x13438 }, { 0x16AF0, 0x16AF4 },
228 { 0x16B30, 0x16B36 }, { 0x16F4F, 0x16F4F }, { 0x16F8F, 0x16F92 },
229 { 0x1BC9D, 0x1BC9E }, { 0x1BCA0, 0x1BCA3 }, { 0x1D167, 0x1D169 },
230 { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD },
231 { 0x1D242, 0x1D244 }, { 0x1DA00, 0x1DA36 }, { 0x1DA3B, 0x1DA6C },
232 { 0x1DA75, 0x1DA75 }, { 0x1DA84, 0x1DA84 }, { 0x1DA9B, 0x1DA9F },
233 { 0x1DAA1, 0x1DAAF }, { 0x1E000, 0x1E006 }, { 0x1E008, 0x1E018 },
234 { 0x1E01B, 0x1E021 }, { 0x1E023, 0x1E024 }, { 0x1E026, 0x1E02A },
235 { 0x1E130, 0x1E136 }, { 0x1E2AE, 0x1E2AE }, { 0x1E2EC, 0x1E2EF },
236 { 0x1E4EC, 0x1E4EF }, { 0x1E8D0, 0x1E8D6 }, { 0x1E944, 0x1E94A },
237 { 0x1E947, 0x1E94A }, { 0xE0100, 0xE01EF }
238 };
239
240 /* test for 8-bit control characters */
241 if (ucs == 0)
242 return 0;
243 if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
244 return -1;
245
246 /* binary search in table of non-spacing characters */
247 if (bisearch(ucs, combining,
248 sizeof(combining) / sizeof(struct interval) - 1))
249 return 0;
250
251 /* if we arrive here, ucs is not a combining or C0/C1 control character */
252
253 return 1 +
254 (ucs >= 0x1100 &&
255 (ucs <= 0x115f || /* Hangul Jamo init. consonants */
256 ucs == 0x2329 || ucs == 0x232a ||
257 (ucs >= 0x2e80 && ucs <= 0xa4cf &&
258 ucs != 0x303f) || /* CJK ... Yi */
259 (ucs >= 0xac00 && ucs <= 0xd7a3) || /* Hangul Syllables */
260 (ucs >= 0xf900 && ucs <= 0xfaff) || /* CJK Compatibility Ideographs */
261 (ucs >= 0xfe10 && ucs <= 0xfe19) || /* Vertical forms */
262 (ucs >= 0xfe30 && ucs <= 0xfe6f) || /* CJK Compatibility Forms */
263 (ucs >= 0xff00 && ucs <= 0xff60) || /* Fullwidth Forms */
264 (ucs >= 0xffe0 && ucs <= 0xffe6) ||
265 (ucs >= 0x1f300 && ucs <= 0x1f64f) || /* modified: added Emoticons */
266 (ucs >= 0x1f680 && ucs <= 0x1f6ff) || /* modified: added Transport and Map Symbols */
267 (ucs >= 0x1f900 && ucs <= 0x1f9ff) || /* modified: added Supplemental Symbols and Pictographs */
268 (ucs >= 0x20000 && ucs <= 0x2fffd) ||
269 (ucs >= 0x30000 && ucs <= 0x3fffd)));
270}
271
272
273int mk_wcswidth(const mk_wchar_t *pwcs, size_t n) // modified: use mk_wchar_t
274{
275 int w, width = 0;
276
277 for (;*pwcs && n-- > 0; pwcs++)
278 if ((w = mk_wcwidth(*pwcs)) < 0)
279 return -1;
280 else
281 width += w;
282
283 return width;
284}
285
diff --git a/src/wcwidth.h b/src/wcwidth.h
new file mode 100644
index 0000000..f2fee11
--- /dev/null
+++ b/src/wcwidth.h
@@ -0,0 +1,21 @@
1// wcwidth.h
2
3// Windows does not have a wcwidth function, so we use compatibilty code from
4// http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c by Markus Kuhn
5
6#ifndef MK_WCWIDTH_H
7#define MK_WCWIDTH_H
8
9
10#ifdef _WIN32
11#include <stdint.h>
12typedef uint32_t mk_wchar_t; // Windows wchar_t can be 16-bit, we need 32-bit
13#else
14#include <wchar.h>
15typedef wchar_t mk_wchar_t; // Posix wchar_t is 32-bit so just use that
16#endif
17
18int mk_wcwidth(mk_wchar_t ucs);
19int mk_wcswidth(const mk_wchar_t *pwcs, size_t n);
20
21#endif // MK_WCWIDTH_H
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