/// @module system /// Terminal. // 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 // @section terminal #include #include #include #include "compat.h" #include "bitflags.h" #ifdef _WIN32 # include # include # ifndef _MSC_VER # include # include # include # endif #else # include # include # include # include # include # include # include #endif #include // Windows does not have a wcwidth function, so we use compatibilty code from // http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c by Markus Kuhn #include "wcwidth.h" #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, 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 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 *-------------------------------------------------------------------------*/ 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 file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr` @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); if (!SetConsoleMode(console_handle, new_console_mode)) { termFormatError(L, GetLastError(), "failed to set console mode"); return 2; } #else get_console_handle(L); // to validate args #endif lua_pushboolean(L, 1); return 1; } /*** Gets 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`). _Note_: See [setconsolemode documentation](https://learn.microsoft.com/en-us/windows/console/setconsolemode) for more information on the flags. @function getconsoleflags @tparam file file file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr` @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_all_of(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; } #else get_console_handle(L); // to validate args #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 (Posix). The terminal attributes is a table with the following fields: - `iflag` input flags - `oflag` output flags - `lflag` local flags - `cflag` control 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_all_of(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 1) { lua_settop(L, 1); // use one argument, because the second boolean will fail as get_console_flags expects bitflags } HANDLE console_handle = get_console_handle(L, 1); if (console_handle == NULL) { return 2; // error message is already on the stack } #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 if (lua_gettop(L) > 1) { lua_settop(L, 1); // use one argument, because the second boolean will fail as get_console_flags expects bitflags } HANDLE console_handle = get_console_handle(L, 1); if (console_handle == NULL) { return 2; // error message is already on the stack } lua_pushboolean(L, 0); #endif return 1; } /*------------------------------------------------------------------------- * Reading keyboard input *-------------------------------------------------------------------------*/ #ifdef _WIN32 // Define a static buffer for UTF-8 characters static char utf8_buffer[4]; static int utf8_buffer_len = 0; static int utf8_buffer_index = 0; #endif /*** Reads a key from the console non-blocking. This function should not be called directly, but through the `system.readkey` or `system.readansi` functions. It will return the next byte from the input stream, or `nil` if no key was pressed. On Posix, `io.stdin` must be set to non-blocking mode using `setnonblock` and canonical mode must be turned off using `tcsetattr`, before calling this function. Otherwise it will block. No conversions are done on Posix, so the byte read is returned as-is. On Windows this reads a wide character and converts it to UTF-8. Multi-byte sequences will be buffered internally and returned one byte at a time. @function _readkey @treturn[1] integer the byte read from the input stream @treturn[2] nil if no key was pressed @treturn[3] nil on error @treturn[3] string error message @treturn[3] int errnum (on posix) */ static int lst_readkey(lua_State *L) { #ifdef _WIN32 if (utf8_buffer_len > 0) { // Buffer not empty, return the next byte lua_pushinteger(L, (unsigned char)utf8_buffer[utf8_buffer_index]); utf8_buffer_index++; utf8_buffer_len--; // printf("returning from buffer: %d\n", luaL_checkinteger(L, -1)); if (utf8_buffer_len == 0) { utf8_buffer_index = 0; } return 1; } if (!_kbhit()) { return 0; } wchar_t wc = _getwch(); // printf("----\nread wchar_t: %x\n", wc); if (wc == WEOF) { lua_pushnil(L); lua_pushliteral(L, "read error"); return 2; } if (sizeof(wchar_t) == 2) { // printf("2-byte wchar_t\n"); // only 2 bytes wide, not 4 if (wc >= 0xD800 && wc <= 0xDBFF) { // printf("2-byte wchar_t, received high, getting low...\n"); // we got a high surrogate, so we need to read the next one as the low surrogate if (!_kbhit()) { lua_pushnil(L); lua_pushliteral(L, "incomplete surrogate pair"); return 2; } wchar_t wc2 = _getwch(); // printf("read wchar_t 2: %x\n", wc2); if (wc2 == WEOF) { lua_pushnil(L); lua_pushliteral(L, "read error"); return 2; } if (wc2 < 0xDC00 || wc2 > 0xDFFF) { lua_pushnil(L); lua_pushliteral(L, "invalid surrogate pair"); return 2; } // printf("2-byte pair complete now\n"); wchar_t wch_pair[2] = { wc, wc2 }; utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, wch_pair, 2, utf8_buffer, sizeof(utf8_buffer), NULL, NULL); } else { // printf("2-byte wchar_t, no surrogate pair\n"); // not a high surrogate, so we can handle just the 2 bytes directly utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8_buffer, sizeof(utf8_buffer), NULL, NULL); } } else { // printf("4-byte wchar_t\n"); // 4 bytes wide, so handle as UTF-32 directly utf8_buffer_len = WideCharToMultiByte(CP_UTF8, 0, &wc, 1, utf8_buffer, sizeof(utf8_buffer), NULL, NULL); } // printf("utf8_buffer_len: %d\n", utf8_buffer_len); utf8_buffer_index = 0; if (utf8_buffer_len <= 0) { lua_pushnil(L); lua_pushliteral(L, "UTF-8 conversion error"); return 2; } lua_pushinteger(L, (unsigned char)utf8_buffer[utf8_buffer_index]); utf8_buffer_index++; utf8_buffer_len--; // printf("returning from buffer: %x\n", luaL_checkinteger(L, -1)); return 1; #else // Posix implementation char ch; ssize_t bytes_read = read(STDIN_FILENO, &ch, 1); if (bytes_read > 0) { lua_pushinteger(L, (unsigned char)ch); return 1; } else if (bytes_read == 0) { return 0; // End of file or stream closed } else { if (errno == EAGAIN || errno == EWOULDBLOCK) { // Resource temporarily unavailable, no data available to read return 0; } else { return pusherror(L, "read error"); } } #endif } /*------------------------------------------------------------------------- * Retrieve terminal size *-------------------------------------------------------------------------*/ /*** Get the size of the terminal in rows and columns. @function termsize @treturn[1] int the number of rows @treturn[1] int the number of columns @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, rows); lua_pushinteger(L, columns); return 2; } /*------------------------------------------------------------------------- * utf8 conversion and support *-------------------------------------------------------------------------*/ // Function to convert a single UTF-8 character to a Unicode code point (uint32_t) // To prevent having to do codepage/locale changes, we use a custom implementation int utf8_to_wchar(const char *utf8, size_t len, mk_wchar_t *codepoint) { if (len == 0) { return -1; // No input provided } unsigned char c = (unsigned char)utf8[0]; if (c <= 0x7F) { *codepoint = c; return 1; } else if ((c & 0xE0) == 0xC0) { if (len < 2) return -1; // Not enough bytes *codepoint = ((utf8[0] & 0x1F) << 6) | (utf8[1] & 0x3F); return 2; } else if ((c & 0xF0) == 0xE0) { if (len < 3) return -1; // Not enough bytes *codepoint = ((utf8[0] & 0x0F) << 12) | ((utf8[1] & 0x3F) << 6) | (utf8[2] & 0x3F); return 3; } else if ((c & 0xF8) == 0xF0) { if (len < 4) return -1; // Not enough bytes *codepoint = ((utf8[0] & 0x07) << 18) | ((utf8[1] & 0x3F) << 12) | ((utf8[2] & 0x3F) << 6) | (utf8[3] & 0x3F); return 4; } else { // Invalid UTF-8 character return -1; } } /*** Get the width of a utf8 character for terminal display. @function utf8cwidth @tparam string utf8_char the utf8 character to check, only the width of the first character will be returned @treturn[1] int the display width in columns of the first character in the string (0 for an empty string) @treturn[2] nil @treturn[2] string error message */ int lst_utf8cwidth(lua_State *L) { const char *utf8_char; size_t utf8_len; utf8_char = luaL_checklstring(L, 1, &utf8_len); int width = 0; mk_wchar_t wc; if (utf8_len == 0) { lua_pushinteger(L, 0); return 1; } // Convert the UTF-8 string to a wide character int bytes_processed = utf8_to_wchar(utf8_char, utf8_len, &wc); if (bytes_processed == -1) { lua_pushnil(L); lua_pushstring(L, "Invalid UTF-8 character"); return 2; } // Get the width of the wide character width = mk_wcwidth(wc); if (width == -1) { lua_pushnil(L); lua_pushstring(L, "Character width determination failed"); return 2; } lua_pushinteger(L, width); return 1; } /*** Get the width of a utf8 string for terminal display. @function utf8swidth @tparam string utf8_string the utf8 string to check @treturn[1] int the display width of the string in columns (0 for an empty string) @treturn[2] nil @treturn[2] string error message */ int lst_utf8swidth(lua_State *L) { const char *utf8_str; size_t utf8_len; utf8_str = luaL_checklstring(L, 1, &utf8_len); int total_width = 0; if (utf8_len == 0) { lua_pushinteger(L, 0); return 1; } int bytes_processed = 0; size_t i = 0; mk_wchar_t wc; while (i < utf8_len) { bytes_processed = utf8_to_wchar(utf8_str + i, utf8_len - i, &wc); if (bytes_processed == -1) { lua_pushnil(L); lua_pushstring(L, "Invalid UTF-8 character"); return 2; } int width = mk_wcwidth(wc); if (width == -1) { lua_pushnil(L); lua_pushstring(L, "Character width determination failed"); return 2; } total_width += width; i += bytes_processed; } lua_pushinteger(L, total_width); return 1; } /*------------------------------------------------------------------------- * Windows codepage functions *-------------------------------------------------------------------------*/ /*** Gets the current console code page (Windows). @function getconsolecp @treturn[1] int the current code page (always 65001 on Posix systems) */ static int lst_getconsolecp(lua_State *L) { unsigned int cp = 65001; #ifdef _WIN32 cp = GetConsoleCP(); #endif lua_pushinteger(L, cp); return 1; } /*** Sets the current console code page (Windows). @function setconsolecp @tparam int cp the code page to set, use `system.CODEPAGE_UTF8` (65001) for UTF-8 @treturn[1] bool `true` on success (always `true` on Posix systems) */ static int lst_setconsolecp(lua_State *L) { unsigned int cp = (unsigned int)luaL_checkinteger(L, 1); int success = TRUE; #ifdef _WIN32 SetConsoleCP(cp); #endif lua_pushboolean(L, success); return 1; } /*** Gets the current console output code page (Windows). @function getconsoleoutputcp @treturn[1] int the current code page (always 65001 on Posix systems) */ static int lst_getconsoleoutputcp(lua_State *L) { unsigned int cp = 65001; #ifdef _WIN32 cp = GetConsoleOutputCP(); #endif lua_pushinteger(L, cp); return 1; } /*** Sets the current console output code page (Windows). @function setconsoleoutputcp @tparam int cp the code page to set, use `system.CODEPAGE_UTF8` (65001) for UTF-8 @treturn[1] bool `true` on success (always `true` on Posix systems) */ static int lst_setconsoleoutputcp(lua_State *L) { unsigned int cp = (unsigned int)luaL_checkinteger(L, 1); int success = TRUE; #ifdef _WIN32 SetConsoleOutputCP(cp); #endif lua_pushboolean(L, success); return 1; } /*------------------------------------------------------------------------- * Initializes module *-------------------------------------------------------------------------*/ static luaL_Reg func[] = { { "isatty", lst_isatty }, { "getconsoleflags", lst_getconsoleflags }, { "setconsoleflags", lst_setconsoleflags }, { "tcgetattr", lst_tcgetattr }, { "tcsetattr", lst_tcsetattr }, { "getnonblock", lst_getnonblock }, { "setnonblock", lst_setnonblock }, { "_readkey", lst_readkey }, { "termsize", lst_termsize }, { "utf8cwidth", lst_utf8cwidth }, { "utf8swidth", lst_utf8swidth }, { "getconsolecp", lst_getconsolecp }, { "setconsolecp", lst_setconsolecp }, { "getconsoleoutputcp", lst_getconsoleoutputcp }, { "setconsoleoutputcp", lst_setconsoleoutputcp }, { 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); }