summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs <thijs@thijsschreijer.nl>2023-11-16 09:09:54 +0100
committerThijs Schreijer <thijs@thijsschreijer.nl>2024-04-30 09:28:01 +0200
commitbd994461ef7c2553da9a6945c685152bad50eb8f (patch)
tree28adc32712f00a200a34357e731a570bf1a359dc
parent47c24eed0191f8f72646be63dee94ac2b35eb062 (diff)
downloadluasystem-bd994461ef7c2553da9a6945c685152bad50eb8f.zip
feat(term): getting/setting terminal config flags
-rw-r--r--Makefile10
-rw-r--r--config.ld2
-rw-r--r--examples/compat.lua37
-rw-r--r--examples/flag_debugging.lua7
-rw-r--r--examples/password_input.lua59
-rw-r--r--examples/read.lua119
-rw-r--r--examples/spinner.lua64
-rw-r--r--examples/spiral_snake.lua72
-rw-r--r--luasystem-scm-0.rockspec1
-rw-r--r--spec/04-term_spec.lua20
-rw-r--r--spec/05-bitflags_spec.lua108
-rw-r--r--src/bitflags.c235
-rw-r--r--src/bitflags.h21
-rw-r--r--src/compat.h11
-rw-r--r--src/core.c2
-rw-r--r--src/term.c822
-rw-r--r--system/init.lua212
17 files changed, 1794 insertions, 8 deletions
diff --git a/Makefile b/Makefile
index 4f4d685..dd20ee9 100644
--- a/Makefile
+++ b/Makefile
@@ -45,3 +45,13 @@ install-all:
@cd src && $(MAKE) install LUA_VERSION=5.3
.PHONY: test
+test:
+ busted
+
+.PHONY: lint
+lint:
+ luacheck .
+
+.PHONY: docs
+docs:
+ ldoc .
diff --git a/config.ld b/config.ld
index c13936d..c96d01d 100644
--- a/config.ld
+++ b/config.ld
@@ -8,7 +8,7 @@ style="./doc_topics/"
file={'./src/', './system/'}
topics={'./doc_topics/', './LICENSE.md', './CHANGELOG.md'}
--- examples = {'./examples'}
+examples = {'./examples'}
dir='docs'
sort=true
diff --git a/examples/compat.lua b/examples/compat.lua
new file mode 100644
index 0000000..c00d44a
--- /dev/null
+++ b/examples/compat.lua
@@ -0,0 +1,37 @@
+-- This example shows how to remove platform differences to create a
+-- cross-platform level playing field.
+
+local sys = require "system"
+
+
+
+if sys.is_windows then
+ -- Windows holds multiple copies of environment variables, to ensure `getenv`
+ -- returns what `setenv` sets we need to use the `system.getenv` instead of
+ -- `os.getenv`.
+ os.getenv = sys.getenv -- luacheck: ignore
+
+ -- Set up the terminal to handle ANSI escape sequences on Windows.
+ if sys.isatty(io.stdout) then
+ sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
+ end
+ if sys.isatty(io.stderr) then
+ sys.setconsoleflags(io.stderr, sys.getconsoleflags(io.stderr) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
+ end
+ if sys.isatty(io.stdin) then
+ sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdout) + sys.ENABLE_VIRTUAL_TERMINAL_INPUT)
+ end
+
+
+else
+ -- On Posix, one can set a variable to an empty string, but on Windows, this
+ -- will remove the variable from the environment. To make this consistent
+ -- across platforms, we will remove the variable from the environment if the
+ -- value is an empty string.
+ local old_setenv = sys.setenv
+ function sys.setenv(name, value)
+ if value == "" then value = nil end
+ return old_setenv(name, value)
+ end
+end
+
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 @@
+local sys = require "system"
+
+-- Print the Windows Console flags for stdin
+sys.listconsoleflags(io.stdin)
+
+-- Print the Posix termios flags for stdin
+sys.listtermflags(io.stdin)
diff --git a/examples/password_input.lua b/examples/password_input.lua
new file mode 100644
index 0000000..2994062
--- /dev/null
+++ b/examples/password_input.lua
@@ -0,0 +1,59 @@
+local sys = require "system"
+
+print [[
+
+This example shows how to disable the "echo" of characters read to the console,
+useful for reading secrets from the user.
+
+]]
+
+--- Function to read from stdin without echoing the input (for secrets etc).
+-- It will (in a platform agnostic way) disable echo on the terminal, read the
+-- input, and then re-enable echo.
+-- @param ... Arguments to pass to `io.stdin:read()`
+-- @return the results of `io.stdin:read(...)`
+local function read_secret(...)
+ local w_oldflags, p_oldflags
+
+ if sys.isatty(io.stdin) then
+ -- backup settings, configure echo flags
+ w_oldflags = sys.getconsoleflags(io.stdin)
+ p_oldflags = sys.tcgetattr(io.stdin)
+ -- set echo off to not show password on screen
+ assert(sys.setconsoleflags(io.stdin, w_oldflags - sys.CIF_ECHO_INPUT))
+ assert(sys.tcsetattr(io.stdin, sys.TCSANOW, { lflag = p_oldflags.lflag - sys.L_ECHO }))
+ end
+
+ local secret, err = io.stdin:read(...)
+
+ -- restore settings
+ if sys.isatty(io.stdin) then
+ io.stdout:write("\n") -- Add newline after reading the password
+ sys.setconsoleflags(io.stdin, w_oldflags)
+ sys.tcsetattr(io.stdin, sys.TCSANOW, p_oldflags)
+ end
+
+ return secret, err
+end
+
+
+
+-- Get username
+io.write("Username: ")
+local username = io.stdin:read("*l")
+
+-- Get the secret
+io.write("Password: ")
+local password = read_secret("*l")
+
+-- Get domainname
+io.write("Domain : ")
+local domain = io.stdin:read("*l")
+
+
+-- Print the results
+print("")
+print("Here's what we got:")
+print(" username: " .. username)
+print(" password: " .. password)
+print(" domain : " .. domain)
diff --git a/examples/read.lua b/examples/read.lua
new file mode 100644
index 0000000..7a1c747
--- /dev/null
+++ b/examples/read.lua
@@ -0,0 +1,119 @@
+local sys = require "system"
+
+print [[
+
+This example shows how to do a non-blocking read from the cli.
+
+]]
+
+-- setup Windows console to handle ANSI processing
+local of_in = sys.getconsoleflags(io.stdin)
+local of_out = sys.getconsoleflags(io.stdout)
+sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
+sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT)
+
+-- setup Posix terminal to use non-blocking mode, and disable line-mode
+local of_attr = sys.tcgetattr(io.stdin)
+local of_block = sys.getnonblock(io.stdin)
+sys.setnonblock(io.stdin, true)
+sys.tcsetattr(io.stdin, sys.TCSANOW, {
+ lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, -- disable canonical mode and echo
+})
+
+-- cursor sequences
+local get_cursor_pos = "\27[6n"
+
+
+
+local read_input do
+ local left_over_key
+
+ -- Reads a single key, if it is a 27 (start of ansi escape sequence) then it reads
+ -- the rest of the sequence.
+ -- This function is non-blocking, and will return nil if no key is available.
+ -- In case of an ANSI sequence, it will return the full sequence as a string.
+ -- @return nil|string the key read, or nil if no key is available
+ function read_input()
+ if left_over_key then
+ -- we still have a cached key, return it
+ local key = left_over_key
+ left_over_key = nil
+ return string.char(key)
+ end
+
+ local key = sys.readkey()
+ if key == nil then
+ return nil
+ end
+
+ if key ~= 27 then
+ return string.char(key)
+ end
+
+ -- looks like an ansi escape sequence, immediately read next char
+ -- as an heuristic against manually typing escape sequences
+ local brack = sys.readkey()
+ if brack ~= 91 then
+ -- not the expected [ character, so we return the key as is
+ -- and store the extra key read for the next call
+ left_over_key = brack
+ return string.char(key)
+ end
+
+ -- escape sequence detected, read the rest of the sequence
+ local seq = { key, brack }
+ while true do
+ key = sys.readkey()
+ table.insert(seq, key)
+ if (key >= 65 and key <= 90) or (key >= 97 and key <= 126) then
+ -- end of sequence, return the full sequence
+ return string.char((unpack or table.unpack)(seq))
+ end
+ end
+ -- unreachable
+ end
+end
+
+
+
+print("Press a key, or 'A' to get cursor position, 'ESC' to exit")
+while true do
+ local key
+
+ -- wait for a key, and sleep a bit to not do a busy-wait
+ while not key do
+ key = read_input()
+ if not key then sys.sleep(0.1) end
+ end
+
+ if key == "A" then io.write(get_cursor_pos); io.flush() end
+
+ -- check if we got a key or ANSI sequence
+ if #key == 1 then
+ -- just a key
+ local b = key:byte()
+ if b < 32 then
+ key = "." -- replace control characters with a simple "." to not mess up the screen
+ end
+
+ print("you pressed: " .. key .. " (" .. b .. ")")
+ if b == 27 then
+ print("Escape pressed, exiting")
+ break
+ end
+
+ else
+ -- we got an ANSI sequence
+ local seq = { key:byte(1, #key) }
+ print("ANSI sequence received: " .. key:sub(2,-1), "(bytes: " .. table.concat(seq, ", ")..")")
+ end
+end
+
+
+
+-- Clean up afterwards
+sys.setnonblock(io.stdin, false)
+sys.setconsoleflags(io.stdout, of_out)
+sys.setconsoleflags(io.stdin, of_in)
+sys.tcsetattr(io.stdin, sys.TCSANOW, of_attr)
+sys.setnonblock(io.stdin, of_block)
diff --git a/examples/spinner.lua b/examples/spinner.lua
new file mode 100644
index 0000000..5526adc
--- /dev/null
+++ b/examples/spinner.lua
@@ -0,0 +1,64 @@
+local sys = require("system")
+
+print [[
+
+An example to display a spinner, whilst a long running task executes.
+
+]]
+
+
+-- start make backup, to auto-restore on exit
+sys.autotermrestore()
+-- configure console
+sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT)
+local of = sys.tcgetattr(io.stdin)
+sys.tcsetattr(io.stdin, sys.TCSANOW, { lflag = of.lflag - sys.L_ICANON - sys.L_ECHO })
+sys.setnonblock(io.stdin, true)
+
+
+
+local function hideCursor()
+ io.write("\27[?25l")
+ io.flush()
+end
+
+local function showCursor()
+ io.write("\27[?25h")
+ io.flush()
+end
+
+local function left(n)
+ io.write("\27[",n or 1,"D")
+ io.flush()
+end
+
+
+
+local spinner do
+ local spin = [[|/-\]]
+ local i = 1
+ spinner = function()
+ hideCursor()
+ io.write(spin:sub(i, i))
+ left()
+ i = i + 1
+ if i > #spin then i = 1 end
+
+ if sys.keypressed() then
+ sys.readkey() -- consume key pressed
+ io.write(" ");
+ left()
+ showCursor()
+ return true
+ else
+ return false
+ end
+ end
+end
+
+io.stdout:write("press any key to stop the spinner... ")
+while not spinner() do
+ sys.sleep(0.1)
+end
+
+print("Done!")
diff --git a/examples/spiral_snake.lua b/examples/spiral_snake.lua
new file mode 100644
index 0000000..84a2040
--- /dev/null
+++ b/examples/spiral_snake.lua
@@ -0,0 +1,72 @@
+local sys = require "system"
+
+print [[
+
+This example will draw a snake like spiral on the screen. Showing ANSI escape
+codes for moving the cursor around.
+
+]]
+
+-- backup term settings with auto-restore on exit
+sys.autotermrestore()
+
+-- setup Windows console to handle ANSI processing
+sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
+
+-- start drawing the spiral.
+-- start from current pos, then right, then up, then left, then down, and again.
+local x, y = 1, 1 -- current position
+local dx, dy = 1, 0 -- direction after each step
+local wx, wy = 30, 30 -- width and height of the room
+local mx, my = 1, 1 -- margin
+
+-- commands to move the cursor
+local move_left = "\27[1D"
+local move_right = "\27[1C"
+local move_up = "\27[1A"
+local move_down = "\27[1B"
+
+-- create room: 30 empty lines
+print(("\n"):rep(wy))
+local move = move_right
+
+while wx > 0 and wy > 0 do
+ sys.sleep(0.01) -- slow down the drawing a little
+ io.write("*" .. move_left .. move )
+ io.flush()
+ x = x + dx
+ y = y + dy
+
+ if x > wx and move == move_right then
+ -- end of move right
+ dx = 0
+ dy = 1
+ move = move_up
+ wy = wy - 1
+ my = my + 1
+ elseif y > wy and move == move_up then
+ -- end of move up
+ dx = -1
+ dy = 0
+ move = move_left
+ wx = wx - 1
+ mx = mx + 1
+ elseif x < mx and move == move_left then
+ -- end of move left
+ dx = 0
+ dy = -1
+ move = move_down
+ wy = wy - 1
+ my = my + 1
+ elseif y < my and move == move_down then
+ -- end of move down
+ dx = 1
+ dy = 0
+ move = move_right
+ wx = wx - 1
+ mx = mx + 1
+ end
+end
+
+io.write(move_down:rep(15))
+print("\nDone!")
diff --git a/luasystem-scm-0.rockspec b/luasystem-scm-0.rockspec
index ff5af61..dac3d9b 100644
--- a/luasystem-scm-0.rockspec
+++ b/luasystem-scm-0.rockspec
@@ -59,6 +59,7 @@ local function make_platform(plat)
'src/environment.c',
'src/random.c',
'src/term.c',
+ 'src/bitflags.c',
},
defines = defines[plat],
libraries = libraries[plat],
diff --git a/spec/04-term_spec.lua b/spec/04-term_spec.lua
index a2034aa..9ca37e9 100644
--- a/spec/04-term_spec.lua
+++ b/spec/04-term_spec.lua
@@ -91,4 +91,24 @@ describe("Terminal:", function()
end)
+
+
+ describe("getconsoleflags()", function()
+
+ pending("returns the consoleflags, if called without flags", function()
+print"1"
+package.loaded["system"] = nil
+package.loaded["system.core"] = nil
+print"2"
+local system = require "system"
+print"3"
+for k,v in pairs(system) do print(k,v) end
+for k,v in pairs(debug.getinfo(system.isatty)) do print(k,v) end
+
+ local flags, err = system.getconsoleflags(io.stdin)
+ assert.is_nil(err)
+ assert.is_integer(flags)
+ end)
+
+ end)
end)
diff --git a/spec/05-bitflags_spec.lua b/spec/05-bitflags_spec.lua
new file mode 100644
index 0000000..01bf958
--- /dev/null
+++ b/spec/05-bitflags_spec.lua
@@ -0,0 +1,108 @@
+describe("BitFlags library", function()
+
+ local sys = require("system")
+
+ it("creates new flag objects", function()
+ local bf = sys.bitflag(255)
+ assert.is_not_nil(bf)
+ assert.are.equal(255, bf:value())
+ assert.is.userdata(bf)
+ end)
+
+ it("converts to a hex string", function()
+ local bf = sys.bitflag(255)
+ assert.are.equal("bitflags: 255", tostring(bf))
+ end)
+
+ it("handles OR/ADD operations", function()
+ -- one at a time
+ local bf1 = sys.bitflag(1) -- b0001
+ local bf2 = sys.bitflag(2) -- b0010
+ local bf3 = bf1 + bf2 -- b0011
+ assert.are.equal(3, bf3:value())
+ -- multiple at once
+ local bf4 = sys.bitflag(4+8) -- b1100
+ local bf5 = bf3 + bf4 -- b1111
+ assert.are.equal(15, bf5:value())
+ -- multiple that were already set
+ local bf6 = sys.bitflag(15) -- b1111
+ local bf7 = sys.bitflag(8+2) -- b1010
+ local bf8 = bf6 + bf7 -- b1111
+ assert.are.equal(15, bf8:value())
+ end)
+
+ it("handles AND-NOT/SUBSTRACT operations", function()
+ -- one at a time
+ local bf1 = sys.bitflag(3) -- b0011
+ local bf2 = sys.bitflag(1) -- b0001
+ local bf3 = bf1 - bf2 -- b0010
+ assert.are.equal(2, bf3:value())
+ -- multiple at once
+ local bf4 = sys.bitflag(15) -- b1111
+ local bf5 = sys.bitflag(8+2) -- b1010
+ local bf6 = bf4 - bf5 -- b0101
+ assert.are.equal(5, bf6:value())
+ -- multiple that were not set
+ local bf7 = sys.bitflag(3) -- b0011
+ local bf8 = sys.bitflag(15) -- b1111
+ local bf9 = bf7 - bf8 -- b0000
+ assert.are.equal(0, bf9:value())
+ end)
+
+ it("checks for equality", function()
+ local bf1 = sys.bitflag(4)
+ local bf2 = sys.bitflag(4)
+ local bf3 = sys.bitflag(5)
+ assert.is.True(bf1 == bf2)
+ assert.is.False(bf1 == bf3)
+ end)
+
+ it("indexes bits correctly", function()
+ local bf = sys.bitflag(4) -- b100
+ assert.is_true(bf[2])
+ assert.is_false(bf[1])
+ end)
+
+ it("errors on reading invalid bit indexes", function()
+ local bf = sys.bitflag(4)
+ assert.has_error(function() return bf[-10] end, "index out of range")
+ assert.has_error(function() return bf[10000] end, "index out of range")
+ assert.has_no_error(function() return bf.not_a_number end)
+ end)
+
+ it("sets and clears bits correctly", function()
+ local bf = sys.bitflag(0)
+ bf[1] = true
+ assert.is_true(bf[1])
+ bf[1] = false
+ assert.is_false(bf[1])
+ end)
+
+ it("errors on setting invalid bit indexes", function()
+ local bf = sys.bitflag(0)
+ assert.has_error(function() bf[-10] = true end, "index out of range")
+ assert.has_error(function() bf[10000] = true end, "index out of range")
+ assert.has_error(function() bf.not_a_number = true end, "index must be a number")
+ end)
+
+ it("handles <= and >= operations", function()
+ local bf1 = sys.bitflag(3) -- b0011
+ local bf2 = sys.bitflag(15) -- b1111
+ assert.is_true(bf2 >= bf1) -- all bits in bf1 are set in bf2
+ assert.is_true(bf2 > bf1) -- all bits in bf1 are set in bf2 and some more
+ assert.is_false(bf2 <= bf1) -- not all bits in bf2 are set in bf1
+ assert.is_false(bf2 < bf1) -- not all bits in bf2 are set in bf1
+ end)
+
+ it("checks for a subset using 'has'", function()
+ local bf1 = sys.bitflag(3) -- b0011
+ local bf2 = sys.bitflag(3) -- b0011
+ local bf3 = sys.bitflag(15) -- b1111
+ local bf0 = sys.bitflag(0) -- b0000
+ assert.is_true(bf1:has(bf2)) -- equal
+ assert.is_true(bf3:has(bf1)) -- is a subset, and has more flags
+ assert.is_false(bf1:has(bf3)) -- not a subset, bf3 has more flags
+ assert.is_false(bf1:has(bf0)) -- bf0 is unset, always returns false
+ end)
+
+end)
diff --git a/src/bitflags.c b/src/bitflags.c
new file mode 100644
index 0000000..89a88b7
--- /dev/null
+++ b/src/bitflags.c
@@ -0,0 +1,235 @@
+/// Bitflags module.
+// The bitflag object makes it easy to manipulate flags in a bitmask.
+//
+// It has metamethods that do the hard work, adding flags sets them, substracting
+// unsets them. Comparing flags checks if all flags in the second set are also set
+// in the first set. The `has` method checks if all flags in the second set are
+// also set in the first set, but behaves slightly different.
+//
+// Indexing allows checking values or setting them by bit index (eg. 0-7 for flags
+// in the first byte).
+//
+// _NOTE_: unavailable flags (eg. Windows flags on a Posix system) should not be
+// omitted, but be assigned a value of 0. This is because the `has` method will
+// return `false` if the flags are checked and the value is 0.
+//
+// See `system.bitflag` (the constructor) for extensive examples on usage.
+// @classmod bitflags
+#include "bitflags.h"
+
+#define BITFLAGS_MT_NAME "LuaSystem.BitFlags"
+
+typedef struct {
+ LSBF_BITFLAG flags;
+} LS_BitFlags;
+
+/// Bit flags.
+// Bitflag objects can be used to easily manipulate and compare bit flags.
+// These are primarily for use with the terminal functions, but can be used
+// in other places as well.
+// @section bitflags
+
+
+// pushes a new LS_BitFlags object with the given value onto the stack
+void lsbf_pushbitflags(lua_State *L, LSBF_BITFLAG value) {
+ LS_BitFlags *obj = (LS_BitFlags *)lua_newuserdata(L, sizeof(LS_BitFlags));
+ if (!obj) luaL_error(L, "Memory allocation failed");
+ luaL_getmetatable(L, BITFLAGS_MT_NAME);
+ lua_setmetatable(L, -2);
+ obj->flags = value;
+}
+
+// gets the LS_BitFlags value at the given index. Returns a Lua error if it is not
+// a LS_BitFlags object.
+LSBF_BITFLAG lsbf_checkbitflags(lua_State *L, int index) {
+ LS_BitFlags *obj = (LS_BitFlags *)luaL_checkudata(L, index, BITFLAGS_MT_NAME);
+ return obj->flags;
+}
+
+/***
+Creates a new bitflag object from the given value.
+@function system.bitflag
+@tparam[opt=0] number value the value to create the bitflag object from.
+@treturn bitflag bitflag object with the given values set.
+@usage
+local sys = require 'system'
+local flags = sys.bitflag(2) -- b0010
+
+-- get state of individual bits
+print(flags[0]) -- false
+print(flags[1]) -- true
+
+-- set individual bits
+flags[0] = true -- b0011
+print(flags:value()) -- 3
+print(flags) -- "bitflags: 3"
+
+-- adding flags (bitwise OR)
+local flags1 = sys.bitflag(1) -- b0001
+local flags2 = sys.bitflag(2) -- b0010
+local flags3 = flags1 + flags2 -- b0011
+
+-- substracting flags (bitwise AND NOT)
+print(flags3:value()) -- 3
+flag3 = flag3 - flag3 -- b0000
+print(flags3:value()) -- 0
+
+-- comparing flags
+local flags4 = sys.bitflag(7) -- b0111
+local flags5 = sys.bitflag(255) -- b11111111
+print(flags5 >= flags4) -- true, all bits in flags4 are set in flags5
+
+-- comparing with 0 flags: comparison and `has` behave differently
+local flags6 = sys.bitflag(0) -- b0000
+local flags7 = sys.bitflag(1) -- b0001
+print(flags6 < flags7) -- true, flags6 is a subset of flags7
+print(flags7:has(flags6)) -- false, flags6 is not set in flags7
+*/
+static int lsbf_new(lua_State *L) {
+ LSBF_BITFLAG flags = 0;
+ if (lua_gettop(L) > 0) {
+ flags = luaL_checkinteger(L, 1);
+ }
+ lsbf_pushbitflags(L, flags);
+ return 1;
+}
+
+/***
+Retrieves the numeric value of the bitflag object.
+@function bitflag:value
+@treturn number the numeric value of the bitflags.
+@usage
+local sys = require 'system'
+local flags = sys.bitflag() -- b0000
+flags[0] = true -- b0001
+flags[2] = true -- b0101
+print(flags:value()) -- 5
+*/
+static int lsbf_value(lua_State *L) {
+ lua_pushinteger(L, lsbf_checkbitflags(L, 1));
+ return 1;
+}
+
+static int lsbf_tostring(lua_State *L) {
+ lua_pushfstring(L, "bitflags: %d", lsbf_checkbitflags(L, 1));
+ return 1;
+}
+
+static int lsbf_add(lua_State *L) {
+ lsbf_pushbitflags(L, lsbf_checkbitflags(L, 1) | lsbf_checkbitflags(L, 2));
+ return 1;
+}
+
+static int lsbf_sub(lua_State *L) {
+ lsbf_pushbitflags(L, lsbf_checkbitflags(L, 1) & ~lsbf_checkbitflags(L, 2));
+ return 1;
+}
+
+static int lsbf_eq(lua_State *L) {
+ lua_pushboolean(L, lsbf_checkbitflags(L, 1) == lsbf_checkbitflags(L, 2));
+ return 1;
+}
+
+static int lsbf_le(lua_State *L) {
+ LSBF_BITFLAG a = lsbf_checkbitflags(L, 1);
+ LSBF_BITFLAG b = lsbf_checkbitflags(L, 2);
+ // Check if all bits in b are also set in a
+ lua_pushboolean(L, (a & b) == a);
+ return 1;
+}
+
+/***
+Checks if the given flags are set.
+This is different from the `>=` and `<=` operators because if the flag to check
+has a value `0`, it will always return `false`. So if there are flags that are
+unsupported on a platform, they can be set to 0 and the `has` function will
+return `false` if the flags are checked.
+@function bitflag:has
+@tparam bitflag subset the flags to check for.
+@treturn boolean true if all the flags are set, false otherwise.
+@usage
+local sys = require 'system'
+local flags = sys.bitflag(12) -- b1100
+local myflags = sys.bitflag(15) -- b1111
+print(flags:has(myflags)) -- false, not all bits in myflags are set in flags
+print(myflags:has(flags)) -- true, all bits in flags are set in myflags
+*/
+static int lsbf_has(lua_State *L) {
+ LSBF_BITFLAG a = lsbf_checkbitflags(L, 1);
+ LSBF_BITFLAG b = lsbf_checkbitflags(L, 2);
+ // Check if all bits in b are also set in a, and b is not 0
+ lua_pushboolean(L, (a | b) == a && b != 0);
+ return 1;
+}
+
+static int lsbf_lt(lua_State *L) {
+ LSBF_BITFLAG a = lsbf_checkbitflags(L, 1);
+ LSBF_BITFLAG b = lsbf_checkbitflags(L, 2);
+ // Check if a is strictly less than b, meaning a != b and a is a subset of b
+ lua_pushboolean(L, (a != b) && ((a & b) == a));
+ return 1;
+}
+
+static int lsbf_index(lua_State *L) {
+ if (!lua_isnumber(L, 2)) {
+ // the parameter isn't a number, just lookup the key in the metatable
+ lua_getmetatable(L, 1);
+ lua_pushvalue(L, 2);
+ lua_gettable(L, -2);
+ return 1;
+ }
+
+ int index = luaL_checkinteger(L, 2);
+ if (index < 0 || index >= sizeof(LSBF_BITFLAG) * 8) {
+ return luaL_error(L, "index out of range");
+ }
+ lua_pushboolean(L, (lsbf_checkbitflags(L, 1) & (1 << index)) != 0);
+ return 1;
+}
+
+static int lsbf_newindex(lua_State *L) {
+ LS_BitFlags *obj = (LS_BitFlags *)luaL_checkudata(L, 1, BITFLAGS_MT_NAME);
+
+ if (!lua_isnumber(L, 2)) {
+ return luaL_error(L, "index must be a number");
+ }
+ int index = luaL_checkinteger(L, 2);
+ if (index < 0 || index >= sizeof(LSBF_BITFLAG) * 8) {
+ return luaL_error(L, "index out of range");
+ }
+
+ luaL_checkany(L, 3);
+ if (lua_toboolean(L, 3)) {
+ obj->flags |= (1 << index);
+ } else {
+ obj->flags &= ~(1 << index);
+ }
+ return 0;
+}
+
+static const struct luaL_Reg lsbf_funcs[] = {
+ {"bitflag", lsbf_new},
+ {NULL, NULL}
+};
+
+static const struct luaL_Reg lsbf_methods[] = {
+ {"value", lsbf_value},
+ {"has", lsbf_has},
+ {"__tostring", lsbf_tostring},
+ {"__add", lsbf_add},
+ {"__sub", lsbf_sub},
+ {"__eq", lsbf_eq},
+ {"__le", lsbf_le},
+ {"__lt", lsbf_lt},
+ {"__index", lsbf_index},
+ {"__newindex", lsbf_newindex},
+ {NULL, NULL}
+};
+
+void bitflags_open(lua_State *L) {
+ luaL_newmetatable(L, BITFLAGS_MT_NAME);
+ luaL_setfuncs(L, lsbf_methods, 0);
+ lua_pop(L, 1);
+
+ luaL_setfuncs(L, lsbf_funcs, 0);
+}
diff --git a/src/bitflags.h b/src/bitflags.h
new file mode 100644
index 0000000..0e47246
--- /dev/null
+++ b/src/bitflags.h
@@ -0,0 +1,21 @@
+#ifndef LSBITFLAGS_H
+#define LSBITFLAGS_H
+
+#include <lua.h>
+#include "compat.h"
+#include <lauxlib.h>
+#include <stdlib.h>
+
+// type used to store the bitflags
+#define LSBF_BITFLAG lua_Integer
+
+// Validates that the given index is a bitflag object and returns its value.
+// If the index is not a bitflag object, a Lua error is raised.
+// The value will be left on the stack.
+LSBF_BITFLAG lsbf_checkbitflags(lua_State *L, int index);
+
+// Pushes a new bitflag object with the given value onto the stack.
+// Might raise a Lua error if memory allocation fails.
+void lsbf_pushbitflags(lua_State *L, LSBF_BITFLAG value);
+
+#endif
diff --git a/src/compat.h b/src/compat.h
index 35f9ef2..7a1fcee 100644
--- a/src/compat.h
+++ b/src/compat.h
@@ -13,6 +13,17 @@ void luaL_setfuncs(lua_State *L, const luaL_Reg *l, int nup);
#include <sys/types.h>
#endif
+// Windows compatibility; define DWORD and TRUE/FALSE on non-Windows
+#ifndef _WIN32
+#ifndef DWORD
+#define DWORD unsigned long
+#endif
+#ifndef TRUE
+#define TRUE 1
+#define FALSE 0
+#endif
+#endif
+
#ifdef _MSC_VER
// MSVC Windows doesn't have ssize_t, so we define it here
#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);
void environment_open(lua_State *L);
void random_open(lua_State *L);
void term_open(lua_State *L);
+void bitflags_open(lua_State *L);
/*-------------------------------------------------------------------------
* Initializes all library modules.
@@ -32,6 +33,7 @@ LUAEXPORT int luaopen_system_core(lua_State *L) {
lua_pushboolean(L, 0);
#endif
lua_rawset(L, -3);
+ bitflags_open(L); // must be first, used by others
time_open(L);
random_open(L);
term_open(L);
diff --git a/src/term.c b/src/term.c
index 2adb1e9..062394d 100644
--- a/src/term.c
+++ b/src/term.c
@@ -1,37 +1,849 @@
/// @submodule system
+
+// Unix: see https://blog.nelhage.com/2009/12/a-brief-introduction-to-termios-termios3-and-stty/
+// Windows: see https://learn.microsoft.com/en-us/windows/console/console-reference
+
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include "compat.h"
+#include "bitflags.h"
#ifndef _MSC_VER
# include <unistd.h>
#endif
+#ifdef _WIN32
+# include <windows.h>
+#else
+# include <termios.h>
+# include <string.h>
+# include <errno.h>
+# include <fcntl.h>
+# include <sys/ioctl.h>
+# include <unistd.h>
+#endif
+
+#ifdef _WIN32
+// after an error is returned, GetLastError() result can be passed to this function to get a string
+// representation of the error on the stack.
+// result will be nil+error on the stack, always 2 results.
+static void termFormatError(lua_State *L, DWORD errorCode, const char* prefix) {
+//static void FormatErrorAndReturn(lua_State *L, DWORD errorCode, const char* prefix) {
+ LPSTR messageBuffer = NULL;
+ FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, errorCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);
+
+ lua_pushnil(L);
+ if (messageBuffer) {
+ if (prefix) {
+ lua_pushfstring(L, "%s: %s", prefix, messageBuffer);
+ } else {
+ lua_pushstring(L, messageBuffer);
+ }
+ LocalFree(messageBuffer);
+ } else {
+ lua_pushfstring(L, "%sError code %d", prefix ? prefix : "", errorCode);
+ }
+}
+#else
+static int pusherror(lua_State *L, const char *info)
+{
+ lua_pushnil(L);
+ if (info==NULL)
+ lua_pushstring(L, strerror(errno));
+ else
+ lua_pushfstring(L, "%s: %s", info, strerror(errno));
+ lua_pushinteger(L, errno);
+ return 3;
+}
+#endif
/***
Checks if a file-handle is a TTY.
@function isatty
-@tparam file file the file-handle to check
+@tparam file file the file-handle to check, one of `io.stdin`, `io.stdout`, `io.stderr`.
@treturn boolean true if the file is a tty
+@usage
+local system = require('system')
+if system.isatty(io.stdin) then
+ -- enable ANSI coloring etc on Windows, does nothing in Posix.
+ local flags = system.getconsoleflags(io.stdout)
+ system.setconsoleflags(io.stdout, flags + sys.COF_VIRTUAL_TERMINAL_PROCESSING)
+end
*/
-static int lua_isatty(lua_State* L) {
+static int lst_isatty(lua_State* L) {
FILE **fh = (FILE **) luaL_checkudata(L, 1, LUA_FILEHANDLE);
lua_pushboolean(L, isatty(fileno(*fh)));
return 1;
}
+/*-------------------------------------------------------------------------
+ * Windows Get/SetConsoleMode functions
+ *-------------------------------------------------------------------------*/
-static luaL_Reg func[] = {
- { "isatty", lua_isatty },
- { NULL, NULL }
+typedef struct ls_RegConst {
+ const char *name;
+ DWORD value;
+} ls_RegConst;
+
+// Define a macro to check if a constant is defined and set it to 0 if not.
+// This is needed because some flags are not defined on all platforms. So we
+// still export the constants, but they will be all 0, and hence not do anything.
+#ifdef _WIN32
+#define CHECK_WIN_FLAG_OR_ZERO(flag) flag
+#define CHECK_NIX_FLAG_OR_ZERO(flag) 0
+#else
+#define CHECK_WIN_FLAG_OR_ZERO(flag) 0
+#define CHECK_NIX_FLAG_OR_ZERO(flag) flag
+#endif
+
+// Export Windows constants to Lua
+static const struct ls_RegConst win_console_in_flags[] = {
+ // Console Input Flags
+ {"CIF_ECHO_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_ECHO_INPUT)},
+ {"CIF_INSERT_MODE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_INSERT_MODE)},
+ {"CIF_LINE_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_LINE_INPUT)},
+ {"CIF_MOUSE_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_MOUSE_INPUT)},
+ {"CIF_PROCESSED_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_PROCESSED_INPUT)},
+ {"CIF_QUICK_EDIT_MODE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_QUICK_EDIT_MODE)},
+ {"CIF_WINDOW_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_WINDOW_INPUT)},
+ {"CIF_VIRTUAL_TERMINAL_INPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_VIRTUAL_TERMINAL_INPUT)},
+ {"CIF_EXTENDED_FLAGS", CHECK_WIN_FLAG_OR_ZERO(ENABLE_EXTENDED_FLAGS)},
+ {"CIF_AUTO_POSITION", CHECK_WIN_FLAG_OR_ZERO(ENABLE_AUTO_POSITION)},
+ {NULL, 0}
+};
+
+static const struct ls_RegConst win_console_out_flags[] = {
+ // Console Output Flags
+ {"COF_PROCESSED_OUTPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_PROCESSED_OUTPUT)},
+ {"COF_WRAP_AT_EOL_OUTPUT", CHECK_WIN_FLAG_OR_ZERO(ENABLE_WRAP_AT_EOL_OUTPUT)},
+ {"COF_VIRTUAL_TERMINAL_PROCESSING", CHECK_WIN_FLAG_OR_ZERO(ENABLE_VIRTUAL_TERMINAL_PROCESSING)},
+ {"COF_DISABLE_NEWLINE_AUTO_RETURN", CHECK_WIN_FLAG_OR_ZERO(DISABLE_NEWLINE_AUTO_RETURN)},
+ {"COF_ENABLE_LVB_GRID_WORLDWIDE", CHECK_WIN_FLAG_OR_ZERO(ENABLE_LVB_GRID_WORLDWIDE)},
+ {NULL, 0}
+};
+
+
+// Export Unix constants to Lua
+static const struct ls_RegConst nix_tcsetattr_actions[] = {
+ // The optional actions for tcsetattr
+ {"TCSANOW", CHECK_NIX_FLAG_OR_ZERO(TCSANOW)},
+ {"TCSADRAIN", CHECK_NIX_FLAG_OR_ZERO(TCSADRAIN)},
+ {"TCSAFLUSH", CHECK_NIX_FLAG_OR_ZERO(TCSAFLUSH)},
+ {NULL, 0}
+};
+
+static const struct ls_RegConst nix_console_i_flags[] = {
+ // Input flags (c_iflag)
+ {"I_IGNBRK", CHECK_NIX_FLAG_OR_ZERO(IGNBRK)},
+ {"I_BRKINT", CHECK_NIX_FLAG_OR_ZERO(BRKINT)},
+ {"I_IGNPAR", CHECK_NIX_FLAG_OR_ZERO(IGNPAR)},
+ {"I_PARMRK", CHECK_NIX_FLAG_OR_ZERO(PARMRK)},
+ {"I_INPCK", CHECK_NIX_FLAG_OR_ZERO(INPCK)},
+ {"I_ISTRIP", CHECK_NIX_FLAG_OR_ZERO(ISTRIP)},
+ {"I_INLCR", CHECK_NIX_FLAG_OR_ZERO(INLCR)},
+ {"I_IGNCR", CHECK_NIX_FLAG_OR_ZERO(IGNCR)},
+ {"I_ICRNL", CHECK_NIX_FLAG_OR_ZERO(ICRNL)},
+#ifndef __APPLE__
+ {"I_IUCLC", CHECK_NIX_FLAG_OR_ZERO(IUCLC)}, // Might not be available on all systems
+#else
+ {"I_IUCLC", 0},
+#endif
+ {"I_IXON", CHECK_NIX_FLAG_OR_ZERO(IXON)},
+ {"I_IXANY", CHECK_NIX_FLAG_OR_ZERO(IXANY)},
+ {"I_IXOFF", CHECK_NIX_FLAG_OR_ZERO(IXOFF)},
+ {"I_IMAXBEL", CHECK_NIX_FLAG_OR_ZERO(IMAXBEL)},
+ {NULL, 0}
};
+static const struct ls_RegConst nix_console_o_flags[] = {
+ // Output flags (c_oflag)
+ {"O_OPOST", CHECK_NIX_FLAG_OR_ZERO(OPOST)},
+#ifndef __APPLE__
+ {"O_OLCUC", CHECK_NIX_FLAG_OR_ZERO(OLCUC)}, // Might not be available on all systems
+#else
+ {"O_OLCUC", 0},
+#endif
+ {"O_ONLCR", CHECK_NIX_FLAG_OR_ZERO(ONLCR)},
+ {"O_OCRNL", CHECK_NIX_FLAG_OR_ZERO(OCRNL)},
+ {"O_ONOCR", CHECK_NIX_FLAG_OR_ZERO(ONOCR)},
+ {"O_ONLRET", CHECK_NIX_FLAG_OR_ZERO(ONLRET)},
+ {"O_OFILL", CHECK_NIX_FLAG_OR_ZERO(OFILL)},
+ {"O_OFDEL", CHECK_NIX_FLAG_OR_ZERO(OFDEL)},
+ {"O_NLDLY", CHECK_NIX_FLAG_OR_ZERO(NLDLY)},
+ {"O_CRDLY", CHECK_NIX_FLAG_OR_ZERO(CRDLY)},
+ {"O_TABDLY", CHECK_NIX_FLAG_OR_ZERO(TABDLY)},
+ {"O_BSDLY", CHECK_NIX_FLAG_OR_ZERO(BSDLY)},
+ {"O_VTDLY", CHECK_NIX_FLAG_OR_ZERO(VTDLY)},
+ {"O_FFDLY", CHECK_NIX_FLAG_OR_ZERO(FFDLY)},
+ {NULL, 0}
+};
+
+static const struct ls_RegConst nix_console_l_flags[] = {
+ // Local flags (c_lflag)
+ {"L_ISIG", CHECK_NIX_FLAG_OR_ZERO(ISIG)},
+ {"L_ICANON", CHECK_NIX_FLAG_OR_ZERO(ICANON)},
+#ifndef __APPLE__
+ {"L_XCASE", CHECK_NIX_FLAG_OR_ZERO(XCASE)}, // Might not be available on all systems
+#else
+ {"L_XCASE", 0},
+#endif
+ {"L_ECHO", CHECK_NIX_FLAG_OR_ZERO(ECHO)},
+ {"L_ECHOE", CHECK_NIX_FLAG_OR_ZERO(ECHOE)},
+ {"L_ECHOK", CHECK_NIX_FLAG_OR_ZERO(ECHOK)},
+ {"L_ECHONL", CHECK_NIX_FLAG_OR_ZERO(ECHONL)},
+ {"L_NOFLSH", CHECK_NIX_FLAG_OR_ZERO(NOFLSH)},
+ {"L_TOSTOP", CHECK_NIX_FLAG_OR_ZERO(TOSTOP)},
+ {"L_ECHOCTL", CHECK_NIX_FLAG_OR_ZERO(ECHOCTL)}, // Might not be available on all systems
+ {"L_ECHOPRT", CHECK_NIX_FLAG_OR_ZERO(ECHOPRT)}, // Might not be available on all systems
+ {"L_ECHOKE", CHECK_NIX_FLAG_OR_ZERO(ECHOKE)}, // Might not be available on all systems
+ {"L_FLUSHO", CHECK_NIX_FLAG_OR_ZERO(FLUSHO)},
+ {"L_PENDIN", CHECK_NIX_FLAG_OR_ZERO(PENDIN)},
+ {"L_IEXTEN", CHECK_NIX_FLAG_OR_ZERO(IEXTEN)},
+ {NULL, 0}
+};
+
+static DWORD win_valid_in_flags = 0;
+static DWORD win_valid_out_flags = 0;
+static DWORD nix_valid_i_flags = 0;
+static DWORD nix_valid_o_flags = 0;
+static DWORD nix_valid_l_flags = 0;
+static void initialize_valid_flags()
+{
+ win_valid_in_flags = 0;
+ for (int i = 0; win_console_in_flags[i].name != NULL; i++)
+ {
+ win_valid_in_flags |= win_console_in_flags[i].value;
+ }
+ win_valid_out_flags = 0;
+ for (int i = 0; win_console_out_flags[i].name != NULL; i++)
+ {
+ win_valid_out_flags |= win_console_out_flags[i].value;
+ }
+ nix_valid_i_flags = 0;
+ for (int i = 0; nix_console_i_flags[i].name != NULL; i++)
+ {
+ nix_valid_i_flags |= nix_console_i_flags[i].value;
+ }
+ nix_valid_o_flags = 0;
+ for (int i = 0; nix_console_o_flags[i].name != NULL; i++)
+ {
+ nix_valid_o_flags |= nix_console_o_flags[i].value;
+ }
+ nix_valid_l_flags = 0;
+ for (int i = 0; nix_console_l_flags[i].name != NULL; i++)
+ {
+ nix_valid_l_flags |= nix_console_l_flags[i].value;
+ }
+}
+
+#ifdef _WIN32
+// first item on the stack should be io.stdin, io.stderr, or io.stdout, second item
+// should be the flags to validate.
+// If it returns NULL, then it leaves nil+err on the stack
+static HANDLE get_console_handle(lua_State *L, int flags_optional)
+{
+ if (lua_gettop(L) < 1) {
+ luaL_argerror(L, 1, "expected file handle");
+ }
+
+ HANDLE handle;
+ DWORD valid;
+ FILE *file = *(FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE);
+ if (file == stdin && file != NULL) {
+ handle = GetStdHandle(STD_INPUT_HANDLE);
+ valid = win_valid_in_flags;
+
+ } else if (file == stdout && file != NULL) {
+ handle = GetStdHandle(STD_OUTPUT_HANDLE);
+ valid = win_valid_out_flags;
+
+ } else if (file == stderr && file != NULL) {
+ handle = GetStdHandle(STD_ERROR_HANDLE);
+ valid = win_valid_out_flags;
+
+ } else {
+ luaL_argerror(L, 1, "invalid file handle"); // does not return
+ }
+
+ if (handle == INVALID_HANDLE_VALUE) {
+ termFormatError(L, GetLastError(), "failed to retrieve std handle");
+ lua_error(L); // does not return
+ }
+
+ if (handle == NULL) {
+ lua_pushnil(L);
+ lua_pushliteral(L, "failed to get console handle");
+ return NULL;
+ }
+
+ if (flags_optional && lua_gettop(L) < 2) {
+ return handle;
+ }
+
+ if (lua_gettop(L) < 2) {
+ luaL_argerror(L, 2, "expected flags");
+ }
+
+ LSBF_BITFLAG flags = lsbf_checkbitflags(L, 2);
+ if ((flags & ~valid) != 0) {
+ luaL_argerror(L, 2, "invalid flags");
+ }
+
+ return handle;
+}
+#else
+// first item on the stack should be io.stdin, io.stderr, or io.stdout. Throws a
+// Lua error if the file is not one of these.
+static int get_console_handle(lua_State *L)
+{
+ FILE **file = (FILE **)luaL_checkudata(L, 1, LUA_FILEHANDLE);
+ if (file == NULL || *file == NULL) {
+ return luaL_argerror(L, 1, "expected file handle"); // call doesn't return
+ }
+
+ // Check if the file is stdin, stdout, or stderr
+ if (*file == stdin || *file == stdout || *file == stderr) {
+ // Push the file descriptor onto the Lua stack
+ return fileno(*file);
+ }
+
+ return luaL_argerror(L, 1, "invalid file handle"); // does not return
+}
+#endif
+
+
+
+/***
+Sets the console flags (Windows).
+The `CIF_` and `COF_` constants are available on the module table. Where `CIF` are the
+input flags (for use with `io.stdin`) and `COF` are the output flags (for use with
+`io.stdout`/`io.stderr`).
+
+To see flag status and constant names check `listconsoleflags`.
+
+Note: not all combinations of flags are allowed, as some are mutually exclusive or mutually required.
+See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
+@function setconsoleflags
+@tparam file file the file-handle to set the flags on
+@tparam bitflags bitflags the flags to set/unset
+@treturn[1] boolean `true` on success
+@treturn[2] nil
+@treturn[2] string error message
+@usage
+local system = require('system')
+system.listconsoleflags(io.stdout) -- List all the available flags and their current status
+
+local flags = system.getconsoleflags(io.stdout)
+assert(system.setconsoleflags(io.stdout,
+ flags + system.COF_VIRTUAL_TERMINAL_PROCESSING)
+
+system.listconsoleflags(io.stdout) -- List again to check the differences
+*/
+static int lst_setconsoleflags(lua_State *L)
+{
+#ifdef _WIN32
+ HANDLE console_handle = get_console_handle(L, 0);
+ if (console_handle == NULL) {
+ return 2; // error message is already on the stack
+ }
+ LSBF_BITFLAG new_console_mode = lsbf_checkbitflags(L, 2);
+
+ DWORD prev_console_mode;
+ if (GetConsoleMode(console_handle, &prev_console_mode) == 0)
+ {
+ termFormatError(L, GetLastError(), "failed to get console mode");
+ return 2;
+ }
+
+ int success = SetConsoleMode(console_handle, new_console_mode) != 0;
+ if (!success)
+ {
+ termFormatError(L, GetLastError(), "failed to set console mode");
+ return 2;
+ }
+
+#endif
+ lua_pushboolean(L, 1);
+ return 1;
+}
+
+
+
+/***
+Gets console flags (Windows).
+@function getconsoleflags
+@tparam file file the file-handle to get the flags from.
+@treturn[1] bitflags the current console flags.
+@treturn[2] nil
+@treturn[2] string error message
+@usage
+local system = require('system')
+
+local flags = system.getconsoleflags(io.stdout)
+print("Current stdout flags:", tostring(flags))
+
+if flags:has(system.COF_VIRTUAL_TERMINAL_PROCESSING + system.COF_PROCESSED_OUTPUT) then
+ print("Both flags are set")
+else
+ print("At least one flag is not set")
+end
+*/
+static int lst_getconsoleflags(lua_State *L)
+{
+ DWORD console_mode = 0;
+
+#ifdef _WIN32
+ HANDLE console_handle = get_console_handle(L, 1);
+ if (console_handle == NULL) {
+ return 2; // error message is already on the stack
+ }
+
+ if (GetConsoleMode(console_handle, &console_mode) == 0)
+ {
+ lua_pushnil(L);
+ lua_pushliteral(L, "failed to get console mode");
+ return 2;
+ }
+
+#endif
+ lsbf_pushbitflags(L, console_mode);
+ return 1;
+}
+
+
+
+/*-------------------------------------------------------------------------
+ * Unix tcgetattr/tcsetattr functions
+ *-------------------------------------------------------------------------*/
+// Code modified from the LuaPosix library by Gary V. Vaughan
+// see https://github.com/luaposix/luaposix
+
+/***
+Get termios state.
+The terminal attributes is a table with the following fields:
+
+- `iflag` input flags
+- `oflag` output flags
+- `cflag` control flags
+- `lflag` local flags
+- `ispeed` input speed
+- `ospeed` output speed
+- `cc` control characters
+
+@function tcgetattr
+@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
+@treturn[1] termios terminal attributes, if successful. On Windows the bitflags are all 0, and the `cc` table is empty.
+@treturn[2] nil
+@treturn[2] string error message
+@treturn[2] int errnum
+@return error message if failed
+@usage
+local system = require('system')
+
+local status = assert(tcgetattr(io.stdin))
+if status.iflag:has(system.I_IGNBRK) then
+ print("Ignoring break condition")
+end
+*/
+static int lst_tcgetattr(lua_State *L)
+{
+#ifndef _WIN32
+ int r, i;
+ struct termios t;
+ int fd = get_console_handle(L);
+
+ r = tcgetattr(fd, &t);
+ if (r == -1) return pusherror(L, NULL);
+
+ lua_newtable(L);
+ lsbf_pushbitflags(L, t.c_iflag);
+ lua_setfield(L, -2, "iflag");
+
+ lsbf_pushbitflags(L, t.c_oflag);
+ lua_setfield(L, -2, "oflag");
+
+ lsbf_pushbitflags(L, t.c_lflag);
+ lua_setfield(L, -2, "lflag");
+
+ lsbf_pushbitflags(L, t.c_cflag);
+ lua_setfield(L, -2, "cflag");
+
+ lua_pushinteger(L, cfgetispeed(&t));
+ lua_setfield(L, -2, "ispeed");
+
+ lua_pushinteger(L, cfgetospeed(&t));
+ lua_setfield(L, -2, "ospeed");
+
+ lua_newtable(L);
+ for (i=0; i<NCCS; i++)
+ {
+ lua_pushinteger(L, i);
+ lua_pushinteger(L, t.c_cc[i]);
+ lua_settable(L, -3);
+ }
+ lua_setfield(L, -2, "cc");
+
+#else
+ lua_newtable(L);
+ lsbf_pushbitflags(L, 0);
+ lua_setfield(L, -2, "iflag");
+ lsbf_pushbitflags(L, 0);
+ lua_setfield(L, -2, "oflag");
+ lsbf_pushbitflags(L, 0);
+ lua_setfield(L, -2, "lflag");
+ lsbf_pushbitflags(L, 0);
+ lua_setfield(L, -2, "cflag");
+ lua_pushinteger(L, 0);
+ lua_setfield(L, -2, "ispeed");
+ lua_pushinteger(L, 0);
+ lua_setfield(L, -2, "ospeed");
+ lua_newtable(L);
+ lua_setfield(L, -2, "cc");
+
+#endif
+ return 1;
+}
+
+
+
+/***
+Set termios state.
+This function will set the flags as given.
+
+The `I_`, `O_`, and `L_` constants are available on the module table. They are the respective
+flags for the `iflags`, `oflags`, and `lflags` bitmasks.
+
+To see flag status and constant names check `listtermflags`. For their meaning check
+[the manpage](https://www.man7.org/linux/man-pages/man3/termios.3.html).
+
+_Note_: not all combinations of flags are allowed, as some are mutually exclusive or mutually required.
+See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode)
+
+_Note_: only `iflag`, `oflag`, and `lflag` are supported at the moment. The other fields are ignored.
+@function tcsetattr
+@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
+@int actions one of `TCSANOW`, `TCSADRAIN`, `TCSAFLUSH`
+@tparam table termios a table with bitflag fields:
+@tparam[opt] bitflags termios.iflag if given will set the input flags
+@tparam[opt] bitflags termios.oflag if given will set the output flags
+@tparam[opt] bitflags termios.lflag if given will set the local flags
+@treturn[1] bool `true`, if successful. Always returns `true` on Windows.
+@return[2] nil
+@treturn[2] string error message
+@treturn[2] int errnum
+@usage
+local system = require('system')
+
+local status = assert(tcgetattr(io.stdin))
+if not status.lflag:has(system.L_ECHO) then
+ -- if echo is off, turn echoing newlines on
+ tcsetattr(io.stdin, system.TCSANOW, { lflag = status.lflag + system.L_ECHONL }))
+end
+*/
+static int lst_tcsetattr(lua_State *L)
+{
+#ifndef _WIN32
+ struct termios t;
+ int r, i;
+ int fd = get_console_handle(L); // first is the console handle
+ int act = luaL_checkinteger(L, 2); // second is the action to take
+ luaL_checktype(L, 3, LUA_TTABLE); // third is the termios table with fields
+
+ r = tcgetattr(fd, &t);
+ if (r == -1) return pusherror(L, NULL);
+
+ lua_getfield(L, 3, "iflag");
+ if (!lua_isnil(L, -1)) {
+ t.c_iflag = lsbf_checkbitflags(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_getfield(L, 3, "oflag");
+ if (!lua_isnil(L, -1)) {
+ t.c_oflag = lsbf_checkbitflags(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_getfield(L, 3, "lflag");
+ if (!lua_isnil(L, -1)) {
+ t.c_lflag = lsbf_checkbitflags(L, -1);
+ }
+ lua_pop(L, 1);
+
+ // Skipping the others for now
+
+ // lua_getfield(L, 3, "cflag"); t.c_cflag = optint(L, -1, 0); lua_pop(L, 1);
+ // lua_getfield(L, 3, "ispeed"); cfsetispeed( &t, optint(L, -1, B0) ); lua_pop(L, 1);
+ // lua_getfield(L, 3, "ospeed"); cfsetospeed( &t, optint(L, -1, B0) ); lua_pop(L, 1);
+
+ // lua_getfield(L, 3, "cc");
+ // for (i=0; i<NCCS; i++)
+ // {
+ // lua_pushinteger(L, i);
+ // lua_gettable(L, -2);
+ // t.c_cc[i] = optint(L, -1, 0);
+ // lua_pop(L, 1);
+ // }
+
+ r = tcsetattr(fd, act, &t);
+ if (r == -1) return pusherror(L, NULL);
+#endif
+
+ lua_pushboolean(L, 1);
+ return 1;
+}
+
+
+
+/***
+Enables or disables non-blocking mode for a file (Posix).
+@function setnonblock
+@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
+@tparam boolean make_non_block a truthy value will enable non-blocking mode, a falsy value will disable it.
+@treturn[1] bool `true`, if successful
+@treturn[2] nil
+@treturn[2] string error message
+@treturn[2] int errnum
+@see getnonblock
+@usage
+local sys = require('system')
+
+-- set io.stdin to non-blocking mode
+local old_setting = sys.getnonblock(io.stdin)
+sys.setnonblock(io.stdin, true)
+
+-- do stuff
+
+-- restore old setting
+sys.setnonblock(io.stdin, old_setting)
+*/
+static int lst_setnonblock(lua_State *L)
+{
+#ifndef _WIN32
+
+ int fd = get_console_handle(L);
+
+ int flags = fcntl(fd, F_GETFL, 0);
+ if (flags == -1) {
+ return pusherror(L, "Error getting handle flags: ");
+ }
+ if (lua_toboolean(L, 2)) {
+ // truthy: set non-blocking
+ flags |= O_NONBLOCK;
+ } else {
+ // falsy: set disable non-blocking
+ flags &= ~O_NONBLOCK;
+ }
+ if (fcntl(fd, F_SETFL, flags) == -1) {
+ return pusherror(L, "Error changing O_NONBLOCK: ");
+ }
+
+#endif
+
+ lua_pushboolean(L, 1);
+ return 1;
+}
+
+
+
+/***
+Gets non-blocking mode status for a file (Posix).
+@function getnonblock
+@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
+@treturn[1] bool `true` if set to non-blocking, `false` if not. Always returns `false` on Windows.
+@treturn[2] nil
+@treturn[2] string error message
+@treturn[2] int errnum
+*/
+static int lst_getnonblock(lua_State *L)
+{
+#ifndef _WIN32
+
+ int fd = get_console_handle(L);
+
+ // Set O_NONBLOCK
+ int flags = fcntl(fd, F_GETFL, 0);
+ if (flags == -1) {
+ return pusherror(L, "Error getting handle flags: ");
+ }
+ if (flags & O_NONBLOCK) {
+ lua_pushboolean(L, 1);
+ } else {
+ lua_pushboolean(L, 0);
+ }
+
+#else
+ lua_pushboolean(L, 0);
+
+#endif
+ return 1;
+}
+
+
+
+/*-------------------------------------------------------------------------
+ * Reading keyboard input
+ *-------------------------------------------------------------------------*/
+
+/***
+Reads a key from the console non-blocking.
+On Posix, `io.stdin` must be set to non-blocking mode using `setnonblock`
+before calling this function. Otherwise it will block.
+
+@function readkey
+@treturn[1] integer the key code of the key that was pressed
+@treturn[2] nil if no key was pressed
+*/
+static int lst_readkey(lua_State *L) {
+#ifdef _WIN32
+ if (_kbhit()) {
+ lua_pushinteger(L, _getch());
+ return 1;
+ }
+ return 0;
+
+#else
+ char ch;
+ if (read(STDIN_FILENO, &ch, 1) > 0) {
+ lua_pushinteger(L, ch);
+ return 1;
+ }
+ return 0;
+
+#endif
+}
+
+/***
+Checks if a key has been pressed without reading it.
+On Posix, `io.stdin` must be set to non-blocking mode using `setnonblock`
+before calling this function. Otherwise it will block.
+
+@function keypressed
+@treturn boolean true if a key has been pressed, nil if not.
+*/
+static int lst_keypressed(lua_State *L) {
+#ifdef _WIN32
+ if (kbhit()) {
+ lua_pushboolean(L, 1);
+ return 1;
+ }
+ return 0;
+
+#else
+ char ch;
+ if (read(STDIN_FILENO, &ch, 1) > 0) {
+ // key was read, push back to stdin
+ ungetc(ch, stdin);
+ lua_pushboolean(L, 1);
+ return 1;
+ }
+ return 0;
+
+#endif
+}
+
+/*-------------------------------------------------------------------------
+ * Retrieve terminal size
+ *-------------------------------------------------------------------------*/
+
+
+/***
+Get the size of the terminal in columns and rows.
+@function termsize
+@treturn[1] int the number of columns
+@treturn[1] int the number of rows
+@treturn[2] nil
+@treturn[2] string error message
+*/
+static int lst_termsize(lua_State *L) {
+ int columns, rows;
+
+#ifdef _WIN32
+ CONSOLE_SCREEN_BUFFER_INFO csbi;
+ if (!GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi)) {
+ termFormatError(L, GetLastError(), "Failed to get terminal size.");
+ return 2;
+ }
+ columns = csbi.srWindow.Right - csbi.srWindow.Left + 1;
+ rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
+
+#else
+ struct winsize ws;
+ if (ioctl(1, TIOCGWINSZ, &ws) == -1) {
+ return pusherror(L, "Failed to get terminal size.");
+ }
+ columns = ws.ws_col;
+ rows = ws.ws_row;
+
+#endif
+ lua_pushinteger(L, columns);
+ lua_pushinteger(L, rows);
+ return 2;
+}
+
+
+
/*-------------------------------------------------------------------------
* Initializes module
*-------------------------------------------------------------------------*/
+
+static luaL_Reg func[] = {
+ { "isatty", lst_isatty },
+ { "getconsoleflags", lst_getconsoleflags },
+ { "setconsoleflags", lst_setconsoleflags },
+ { "tcgetattr", lst_tcgetattr },
+ { "tcsetattr", lst_tcsetattr },
+ { "getnonblock", lst_setnonblock },
+ { "setnonblock", lst_setnonblock },
+ { "readkey", lst_readkey },
+ { "keypressed", lst_keypressed },
+ { "termsize", lst_termsize },
+ { NULL, NULL }
+};
+
+
+
void term_open(lua_State *L) {
+ // set up constants and export the constants in module table
+ initialize_valid_flags();
+ // Windows flags
+ for (int i = 0; win_console_in_flags[i].name != NULL; i++)
+ {
+ lsbf_pushbitflags(L, win_console_in_flags[i].value);
+ lua_setfield(L, -2, win_console_in_flags[i].name);
+ }
+ for (int i = 0; win_console_out_flags[i].name != NULL; i++)
+ {
+ lsbf_pushbitflags(L, win_console_out_flags[i].value);
+ lua_setfield(L, -2, win_console_out_flags[i].name);
+ }
+ // Unix flags
+ for (int i = 0; nix_console_i_flags[i].name != NULL; i++)
+ {
+ lsbf_pushbitflags(L, nix_console_i_flags[i].value);
+ lua_setfield(L, -2, nix_console_i_flags[i].name);
+ }
+ for (int i = 0; nix_console_o_flags[i].name != NULL; i++)
+ {
+ lsbf_pushbitflags(L, nix_console_o_flags[i].value);
+ lua_setfield(L, -2, nix_console_o_flags[i].name);
+ }
+ for (int i = 0; nix_console_l_flags[i].name != NULL; i++)
+ {
+ lsbf_pushbitflags(L, nix_console_l_flags[i].value);
+ lua_setfield(L, -2, nix_console_l_flags[i].name);
+ }
+ // Unix tcsetattr actions
+ for (int i = 0; nix_tcsetattr_actions[i].name != NULL; i++)
+ {
+ lua_pushinteger(L, nix_tcsetattr_actions[i].value);
+ lua_setfield(L, -2, nix_tcsetattr_actions[i].name);
+ }
+
+ // export functions
luaL_setfuncs(L, func, 0);
}
diff --git a/system/init.lua b/system/init.lua
index 77e0c3b..c3ea94d 100644
--- a/system/init.lua
+++ b/system/init.lua
@@ -1,2 +1,210 @@
-local system = require 'system.core'
-return system
+--- Lua System Library.
+-- @module init
+
+local sys = require 'system.core'
+local global_backup -- global backup for terminal settings
+
+
+
+local add_gc_method do
+ -- feature detection; __GC meta-method, not available in all Lua versions
+ local has_gc = false
+ local tt = setmetatable({}, { -- luacheck: ignore
+ __gc = function() has_gc = true end
+ })
+
+ -- clear table and run GC to trigger
+ tt = nil
+ collectgarbage()
+ collectgarbage()
+
+
+ if has_gc then
+ -- use default GC mechanism since it is available
+ function add_gc_method(t, f)
+ setmetatable(t, { __gc = f })
+ end
+ else
+ -- create workaround using a proxy userdata, typical for Lua 5.1
+ function add_gc_method(t, f)
+ local proxy = newproxy(true)
+ getmetatable(proxy).__gc = function()
+ t["__gc_proxy"] = nil
+ f(t)
+ end
+ t["__gc_proxy"] = proxy
+ end
+ end
+end
+
+
+
+--- Returns a backup of terminal setting for stdin/out/err.
+-- Handles terminal/console flags and non-block flags on the streams.
+-- Backs up terminal/console flags only if a stream is a tty.
+-- @return table with backup of terminal settings
+function sys.termbackup()
+ local backup = {}
+
+ if sys.isatty(io.stdin) then
+ backup.console_in = sys.getconsoleflags(io.stdin)
+ backup.term_in = sys.tcgetattr(io.stdin)
+ end
+ if sys.isatty(io.stdout) then
+ backup.console_out = sys.getconsoleflags(io.stdout)
+ backup.term_out = sys.tcgetattr(io.stdout)
+ end
+ if sys.isatty(io.stderr) then
+ backup.console_err = sys.getconsoleflags(io.stderr)
+ backup.term_err = sys.tcgetattr(io.stderr)
+ end
+
+ backup.block_in = sys.getnonblock(io.stdin)
+ backup.block_out = sys.getnonblock(io.stdout)
+ backup.block_err = sys.getnonblock(io.stderr)
+
+ return backup
+end
+
+
+
+--- Restores terminal settings from a backup
+-- @tparam table backup the backup of terminal settings, see `termbackup`.
+-- @treturn boolean true
+function sys.termrestore(backup)
+ if backup.console_in then sys.setconsoleflags(io.stdin, backup.console_in) end
+ if backup.term_in then sys.tcsetattr(io.stdin, sys.TCSANOW, backup.term_in) end
+ if backup.console_out then sys.setconsoleflags(io.stdout, backup.console_out) end
+ if backup.term_out then sys.tcsetattr(io.stdout, sys.TCSANOW, backup.term_out) end
+ if backup.console_err then sys.setconsoleflags(io.stderr, backup.console_err) end
+ if backup.term_err then sys.tcsetattr(io.stderr, sys.TCSANOW, backup.term_err) end
+
+ if backup.block_in ~= nil then sys.setnonblock(io.stdin, backup.block_in) end
+ if backup.block_out ~= nil then sys.setnonblock(io.stdout, backup.block_out) end
+ if backup.block_err ~= nil then sys.setnonblock(io.stderr, backup.block_err) end
+ return true
+end
+
+
+
+--- Backs up terminal settings and restores them on application exit.
+-- Calls `termbackup` to back up terminal settings and sets up a GC method to
+-- automatically restore them on application exit (also works on Lua 5.1).
+-- @treturn[1] boolean true
+-- @treturn[2] nil if the backup was already created
+-- @treturn[2] string error message
+function sys.autotermrestore()
+ if global_backup then
+ return nil, "global terminal backup was already set up"
+ end
+ global_backup = sys.termbackup()
+ add_gc_method(global_backup, function(self)
+ sys.termrestore(self) end)
+ return true
+end
+
+
+
+do
+ local oldunpack = unpack or table.unpack
+ local pack = function(...) return { n = select("#", ...), ... } end
+ local unpack = function(t) return oldunpack(t, 1, t.n) end
+
+ --- Wraps a function to automatically restore terminal settings upon returning.
+ -- Calls `termbackup` before calling the function and `termrestore` after.
+ -- @tparam function f function to wrap
+ -- @treturn function wrapped function
+ function sys.termwrap(f)
+ if type(f) ~= "function" then
+ error("arg #1 to wrap, expected function, got " .. type(f), 2)
+ end
+
+ return function(...)
+ local bu = sys.termbackup()
+ local results = pack(f(...))
+ sys.termrestore(bu)
+ return unpack(results)
+ end
+ end
+end
+
+
+
+--- Debug function for console flags (Windows).
+-- Pretty prints the current flags set for the handle.
+-- @param fh file handle (`io.stdin`, `io.stdout`, `io.stderr`)
+-- @usage -- Print the flags for stdin/out/err
+-- system.listconsoleflags(io.stdin)
+-- system.listconsoleflags(io.stdout)
+-- system.listconsoleflags(io.stderr)
+function sys.listconsoleflags(fh)
+ local flagtype
+ if fh == io.stdin then
+ print "------ STDIN FLAGS WINDOWS ------"
+ flagtype = "CIF_"
+ elseif fh == io.stdout then
+ print "------ STDOUT FLAGS WINDOWS ------"
+ flagtype = "COF_"
+ elseif fh == io.stderr then
+ print "------ STDERR FLAGS WINDOWS ------"
+ flagtype = "COF_"
+ end
+
+ local flags = assert(sys.getconsoleflags(fh))
+ local out = {}
+ for k,v in pairs(sys) do
+ if type(k) == "string" and k:sub(1,4) == flagtype then
+ if flags:has(v) then
+ out[#out+1] = string.format("%10d [x] %s",v:value(),k)
+ else
+ out[#out+1] = string.format("%10d [ ] %s",v:value(),k)
+ end
+ end
+ end
+ table.sort(out)
+ for k,v in pairs(out) do
+ print(v)
+ end
+end
+
+
+
+--- Debug function for terminal flags (Posix).
+-- Pretty prints the current flags set for the handle.
+-- @param fh file handle (`io.stdin`, `io.stdout`, `io.stderr`)
+-- @usage -- Print the flags for stdin/out/err
+-- system.listconsoleflags(io.stdin)
+-- system.listconsoleflags(io.stdout)
+-- system.listconsoleflags(io.stderr)
+function sys.listtermflags(fh)
+ if fh == io.stdin then
+ print "------ STDIN FLAGS POSIX ------"
+ elseif fh == io.stdout then
+ print "------ STDOUT FLAGS POSIX ------"
+ elseif fh == io.stderr then
+ print "------ STDERR FLAGS POSIX ------"
+ end
+
+ local flags = assert(sys.tcgetattr(fh))
+ for _, flagtype in ipairs { "iflag", "oflag", "lflag" } do
+ local prefix = flagtype:sub(1,1):upper() .. "_" -- I_, O_, or L_, the constant prefixes
+ local out = {}
+ for k,v in pairs(sys) do
+ if type(k) == "string" and k:sub(1,2) == prefix then
+ if flags[flagtype]:has(v) then
+ out[#out+1] = string.format("%10d [x] %s",v:value(),k)
+ else
+ out[#out+1] = string.format("%10d [ ] %s",v:value(),k)
+ end
+ end
+ end
+ table.sort(out)
+ for k,v in pairs(out) do
+ print(v)
+ end
+ end
+end
+
+
+
+return sys