diff options
author | Thijs <thijs@thijsschreijer.nl> | 2023-11-16 09:09:54 +0100 |
---|---|---|
committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2024-04-30 09:28:01 +0200 |
commit | bd994461ef7c2553da9a6945c685152bad50eb8f (patch) | |
tree | 28adc32712f00a200a34357e731a570bf1a359dc | |
parent | 47c24eed0191f8f72646be63dee94ac2b35eb062 (diff) | |
download | luasystem-bd994461ef7c2553da9a6945c685152bad50eb8f.zip |
feat(term): getting/setting terminal config flags
-rw-r--r-- | Makefile | 10 | ||||
-rw-r--r-- | config.ld | 2 | ||||
-rw-r--r-- | examples/compat.lua | 37 | ||||
-rw-r--r-- | examples/flag_debugging.lua | 7 | ||||
-rw-r--r-- | examples/password_input.lua | 59 | ||||
-rw-r--r-- | examples/read.lua | 119 | ||||
-rw-r--r-- | examples/spinner.lua | 64 | ||||
-rw-r--r-- | examples/spiral_snake.lua | 72 | ||||
-rw-r--r-- | luasystem-scm-0.rockspec | 1 | ||||
-rw-r--r-- | spec/04-term_spec.lua | 20 | ||||
-rw-r--r-- | spec/05-bitflags_spec.lua | 108 | ||||
-rw-r--r-- | src/bitflags.c | 235 | ||||
-rw-r--r-- | src/bitflags.h | 21 | ||||
-rw-r--r-- | src/compat.h | 11 | ||||
-rw-r--r-- | src/core.c | 2 | ||||
-rw-r--r-- | src/term.c | 822 | ||||
-rw-r--r-- | system/init.lua | 212 |
17 files changed, 1794 insertions, 8 deletions
@@ -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 . @@ -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 @@ -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); @@ -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 |