diff options
Diffstat (limited to 'modules/TUI')
| -rw-r--r-- | modules/TUI/module.jai | 23 | ||||
| -rw-r--r-- | modules/TUI/windows.jai | 266 |
2 files changed, 142 insertions, 147 deletions
diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 2d23bff..7c9d7b1 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -267,21 +267,22 @@ Keys :: struct #type_info_none { active := false; -//input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! -input_buffer : [8] u8; // TODO FIXME Input buffer is too small!!! +input_buffer : [1024] u8; input_string : string; input_override : Key; #run { - // TODO FIXME DEBUG HACK or maybe... let it be?! - // Some tests. - assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire terminal (6 bytes) or UTF8 (4 bytes) code."); + assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire Key, which should be able to hold an UTF8 code (4 bytes) or a terminal escape code (6 bytes)."); } assert_is_active :: inline () { assert(active, "TUI is not ready."); } +//////////////////////////////////////////////////////////////////////////////// + +// #scope_export TODO Setup the scope_export and scope_file + set_next_key :: (key: Key) { assert_is_active(); input_override = key; @@ -295,7 +296,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return input_override; } - if OS_was_terminal_resized() return xx Keys.Resize; + if OS_was_terminal_resized() return Keys.Resize; should_read_input := false; is_input_available := false; @@ -309,7 +310,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { is_input_available = OS_wait_for_input(0); } - if OS_was_terminal_resized() return xx Keys.Resize; + if OS_was_terminal_resized() return Keys.Resize; if should_read_input && is_input_available { // Copy buffered bytes to the start, and read the remaining ones from input. @@ -687,11 +688,3 @@ set_terminal_title :: (title: string) { assert_is_active(); print(Commands.SetWindowTitle, title); } - -// TODO -#if OS == .WINDOWS { - // Prototyping zone... keep clear! -} -else #if OS == .LINUX || OS == .MACOS { - // Prototyping zone... keep clear! -} diff --git a/modules/TUI/windows.jai b/modules/TUI/windows.jai index f79a5cf..5155a3f 100644 --- a/modules/TUI/windows.jai +++ b/modules/TUI/windows.jai @@ -1,9 +1,12 @@ #scope_file #import "Basic"; -#import "Atomics"; #import "System"; #import "Windows"; +#import "Windows_Utf8"; + + // Code page identifiers. + CP_UTF8 :: 65001; // https://learn.microsoft.com/windows/win32/winprog/windows-data-types LPVOID :: *void; @@ -13,42 +16,18 @@ SHORT :: s16; USHORT :: u16; WORD :: u16; - DWORD :: s32; - LPDWORD :: *s32; + DWORD :: u32; + LPDWORD :: *u32; UINT :: u32; - PINPUT_RECORD :: *INPUT_RECORD; -// https://learn.microsoft.com/windows/console/console-virtual-terminal-sequences -// https://learn.microsoft.com/windows/console/console-virtual-terminal-sequences#designate-character-set -// https://github.com/MicrosoftDocs/Console-Docs/blob/main/docs/console-virtual-terminal-sequences.md - - kernel32 :: #system_library "kernel32"; - - // TODO Cleanup unused foreign procedures. - - // https://learn.microsoft.com/windows/console/getconsolescreenbufferinfo - GetConsoleScreenBufferInfo :: (hConsoleOutput: HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) -> bool #foreign kernel32; - // https://learn.microsoft.com/en-us/windows/console/readconsoleinput - ReadConsoleInputA :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> success: bool #foreign kernel32; + ReadConsoleInputW :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> success: bool #foreign kernel32; // https://learn.microsoft.com/windows/console/readconsole - ReadConsoleA :: (hConsoleInput: HANDLE, lpBuffer: LPVOID, nNumberOfCharsToRead: DWORD, lpNumberOfCharsRead: LPVOID, pInputControl := LPVOID) -> success: bool #foreign kernel32; - - // https://learn.microsoft.com/windows/console/getconsolemode - GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *DWORD) -> BOOL #foreign kernel32; - - // https://learn.microsoft.com/windows/console/setconsolemode - SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: DWORD) -> BOOL #foreign kernel32; - - // https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror - GetLastError :: () -> s32 #foreign kernel32; - - // https://learn.microsoft.com/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject - WaitForSingleObject :: (hHandle: HANDLE, dwMilliseconds: DWORD) -> s32 #foreign kernel32; - + ReadConsoleW :: (hConsoleInput: HANDLE, lpBuffer: LPVOID, nNumberOfchars_to_read: DWORD, lpNumberOfchars_read: LPVOID, pInputControl: LPVOID) -> success: bool #foreign kernel32; + // https://learn.microsoft.com/windows/console/flushconsoleinputbuffer FlushConsoleInputBuffer :: (hConsoleInput: HANDLE) -> bool #foreign kernel32; @@ -56,8 +35,8 @@ GetNumberOfConsoleInputEvents :: (hConsoleInput: HANDLE, lpcNumberOfEvents: LPDWORD) -> bool #foreign kernel32; // https://learn.microsoft.com/en-us/windows/console/peekconsoleinput - PeekConsoleInputA :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> bool #foreign kernel32; - + PeekConsoleInputW :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> bool #foreign kernel32; + // https://learn.microsoft.com/en-us/windows/console/setconsolemode // https://learn.microsoft.com/en-us/windows/console/high-level-console-modes Console_Input_Mode :: enum_flags u32 { @@ -65,12 +44,12 @@ ENABLE_LINE_INPUT; // If enable, ReadFile or ReadConsole function return on CR; otherwise they return when one or more characters are available. ENABLE_ECHO_INPUT; // Echoes input on screen. Only available if ENABLE_LINE_INPUT is set. ENABLE_WINDOW_INPUT; - ENABLE_MOUSE_INPUT; // + ENABLE_MOUSE_INPUT; // Makes mouse events available to the ReadConsoleInput function. ENABLE_INSERT_MODE; // If enabled, text entered will be inserted at the current cursor location and all text following that location will not be overwritten. When disabled, all following text will be overwritten. _UNUSED_0040_; _UNUSED_0080_; _UNUSED_0100_; - ENABLE_VIRTUAL_TERMINAL_INPUT; // + ENABLE_VIRTUAL_TERMINAL_INPUT; // If enable, makes user input available to the ReadConsole function. _UNUSED_0400_; _UNUSED_0800_; } @@ -167,124 +146,111 @@ initial_stdout_mode: u32; raw_stdout_mode: Console_Output_Mode; + was_resized: bool; -//////////////////////////////////////////////////////////////////////////////// -// Resize detection -was_resized : bool; - -resize_handler :: (signal_code : s32) #c_call { - /* TODO - Try to implement using: - https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events - https://learn.microsoft.com/en-us/windows/console/readconsoleinput - https://learn.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents - */ - new_context : Context; - push_context new_context { - if signal_code != SIGWINCH then return; - atomic_swap(*was_resized, true); - } -} + windows_buffer: [512] u16; -prepare_resize_handler :: () { - /* TODO - sa : sigaction_t; - sa.sa_handler = resize_handler; - sigemptyset(*(sa.sa_mask)); - sa.sa_flags = SA_RESTART; - sigaction(SIGWINCH, *sa, null); - */ -} -restore_resize_handler :: () { - /* TODO - sa : sigaction_t; - sa.sa_handler = SIG_DFL; - sigaction(SIGWINCH, null, *sa); - */ -} - -peek_input :: inline () -> INPUT_RECORD { +peek_input :: inline () -> INPUT_RECORD, success := true { record: INPUT_RECORD; - records_read: s32; - if PeekConsoleInputA(stdin, *record, 1, *records_read) == false { - _, error_message := get_error_value_and_string(); - assert(false, error_message); + records_read: u32; + if PeekConsoleInputW(stdin, *record, 1, *records_read) == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to peek input: code %, %", error_code, error_string); + return record, false; } return record; } -read_input :: inline () -> INPUT_RECORD { +read_input :: inline () -> INPUT_RECORD, success := true { record: INPUT_RECORD; - records_read: s32; - if ReadConsoleInputA(stdin, *record, 1, *records_read) == false { - _, error_message := get_error_value_and_string(); - assert(false, error_message); + records_read: u32; + if ReadConsoleInputW(stdin, *record, 1, *records_read) == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to read input: code %, %", error_code, error_string); + return record, false; } return record; } -count_input :: inline () -> s32 { - count: s32; +count_input :: inline () -> u32, success := true { + count: u32; if GetNumberOfConsoleInputEvents(stdin, *count) == false { - _, error_message := get_error_value_and_string(); - assert(false, error_message); + error_code, error_string := get_error_value_and_string(); + log_error("Failed to count input: code %, %", error_code, error_string); + return 0, false; } return count; } - //////////////////////////////////////////////////////////////////////////////// #scope_export +// TODO All the log_error calls will be hidden by the terminal setup... we should store the logs internally, or write it to a file. + OS_prepare_terminal :: () { // stdin stdin = GetStdHandle(STD_INPUT_HANDLE); if stdin == INVALID_HANDLE_VALUE { - print("Invalid input handler.", to_standard_error = true); + error_code, error_string := get_error_value_and_string(); + log_error("Invalid input handler: code %, %", error_code, error_string); return; } if xx GetConsoleMode(stdin, *initial_stdin_mode) == false { - print("Failed to get input mode.", to_standard_error = true); + error_code, error_string := get_error_value_and_string(); + log_error("Failed to get input mode: code %, %", error_code, error_string); return; } raw_stdin_mode = (cast(Console_Input_Mode) initial_stdin_mode); raw_stdin_mode |= (.ENABLE_VIRTUAL_TERMINAL_INPUT); raw_stdin_mode &= ~(.ENABLE_LINE_INPUT | .ENABLE_PROCESSED_INPUT | .ENABLE_ECHO_INPUT); - if SetConsoleMode(stdin, xx raw_stdin_mode) == false { - print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); + if xx SetConsoleMode(stdin, xx raw_stdin_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to set input mode: code %, %", error_code, error_string); return; } // stdout stdout = GetStdHandle(STD_OUTPUT_HANDLE); if stdout == INVALID_HANDLE_VALUE { - print("Invalid output handler.", to_standard_error = true); + error_code, error_string := get_error_value_and_string(); + log_error("Invalid output handler: code %, %", error_code, error_string); return; } if xx GetConsoleMode(stdout, *initial_stdout_mode) == false { - print("Failed to get output mode.", to_standard_error = true); + error_code, error_string := get_error_value_and_string(); + log_error("Failed to get output mode: code %, %", error_code, error_string); return; } raw_stdout_mode = (cast(Console_Output_Mode) initial_stdout_mode); raw_stdout_mode |= (.ENABLE_VIRTUAL_TERMINAL_PROCESSING | .ENABLE_PROCESSED_OUTPUT | .ENABLE_WRAP_AT_EOL_OUTPUT); - if SetConsoleMode(stdout, xx raw_stdout_mode) == false { - print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); + if xx SetConsoleMode(stdout, xx raw_stdout_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to set output mode: code %, %", error_code, error_string); return; } + + // Acording to [documentation](https://learn.microsoft.com/en-us/windows/win32/intl/code-pages) + // only the ANSI functions (ending in A) use the Code Page info. The Unicode functions (ending in W) + // already handle Unicode text. + // As long we use the Unicode functions, we shouldn't need to set the code page to UTF8. + // SetConsoleCP(CP_UTF8); + // SetConsoleOutputCP(CP_UTF8); } OS_reset_terminal :: () { if xx SetConsoleMode(stdin, initial_stdin_mode) == false { - print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); + error_code, error_string := get_error_value_and_string(); + log_error("Failed to reset input mode: code %, %", error_code, error_string); return; } if xx SetConsoleMode(stdout, initial_stdout_mode) == false { - print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); + error_code, error_string := get_error_value_and_string(); + log_error("Failed to reset output mode: code %, %", error_code, error_string); return; } } @@ -294,33 +260,73 @@ OS_flush_input :: inline () { This API is not recommended and does not have a virtual terminal equivalent. Attempting to empty the input queue all at once can destroy state in the queue in an unexpected manner. */ - success := FlushConsoleInputBuffer(stdin); - if success == false { - _, error_message := get_error_value_and_string(); - assert(false, error_message); // TODO A bit harsh arent we? + if FlushConsoleInputBuffer(stdin) == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to flush input: code %, %", error_code, error_string); } } -OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bool = false, error_message: string = "" { - - assert(bytes_to_read <= 0x7fff_ffff, "The Windows API only allows to read up to s32 bytes from the standard input."); +OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success := true { - bytes_read: s32 = 0; - available_inputs := count_input(); + S32_MAX :: 0x7fff_ffff; + if (bytes_to_read > S32_MAX) { + log_error("The Windows API only allows to read up to 2^32 bytes from the standard input. Clamping input argument."); + bytes_to_read = S32_MAX; + } + success: bool; + bytes_read: s64 = 0; + available_inputs:, success = count_input(); + while bytes_to_read > 0 && available_inputs > 0 { - record := read_input(); - if record.EventType == .WINDOW_BUFFER_SIZE_EVENT { - was_resized = true; - } + record := peek_input(); + + if record.EventType == { + case .WINDOW_BUFFER_SIZE_EVENT; + was_resized = true; + read_input(); + available_inputs -= 1; + + case .KEY_EVENT; + if record.KeyEvent.bKeyDown == false { + read_input(); + available_inputs -= 1; + } + else { + + chars_view: [] u16; + chars_view.data = windows_buffer.data; + + chars_to_read := ifx available_inputs <= windows_buffer.count then available_inputs else windows_buffer.count; + + success = ReadConsoleW(stdin, chars_view.data, chars_to_read, *chars_view.count, null); + if success == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to read console: code %, %", error_code, error_string); + return 0, false; + } + + result:, success = wide_to_utf8_new(chars_view.data, xx chars_view.count); + if success == false { + error_code, error_string := get_error_value_and_string(); + log_error("Failed to convert from wide to utf8: code %, %", error_code, error_string); + return 0, false; + } + + memcpy(*buffer[bytes_read], result.data, result.count); + + bytes_to_read -= xx result.count; + bytes_read += result.count; - if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true { - buffer[bytes_read] = xx record.KeyEvent.AsciiChar; - bytes_to_read -= 1; - bytes_read += 1; + } + + // Discard other input events. + case; + read_input(); + } - available_inputs -= 1; + available_inputs = count_input(); } return bytes_read; } @@ -328,7 +334,7 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo // timeout_milliseconds // 0: do not wait // -1: wait indefinitely -OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool, success := true { /* TODO Add a good comment explaining how the windows part of the module was implemented... what's the idea behind it. @@ -340,27 +346,22 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0); - // Possible values for poll_return TODO NOT BEING USED - WAIT_ABANDONED :: 0x00000080; // Mutex stuff. + // Possible values for poll_return: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject WAIT_OBJECT_0 :: 0x00000000; // Detected input. WAIT_TIMEOUT :: 0x00000102; // Reached timeout. WAIT_FAILED :: 0xFFFFFFFF; // Something went wrong. - + while true { - poll_return := WaitForSingleObject(stdin, timeout_milliseconds); - - // TODO Weird looking code... - if poll_return == { - case WAIT_TIMEOUT; - return false; - case WAIT_ABANDONED; - print("MUTEX STUFF"); // https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject - return false; - case WAIT_FAILED; - _, error_message := get_error_value_and_string(); - assert(false, error_message); + wait_result := WaitForSingleObject(stdin, cast,no_check(u32)timeout_milliseconds); + + if wait_result == WAIT_FAILED { + error_code, error_string := get_error_value_and_string(); + log_error("Error while waiting for input: code %, %", error_code, error_string); + return false, false; } - + + if wait_result != WAIT_OBJECT_0 then return false; + // Discard invalid input events. count := count_input(); while count > 0 { @@ -369,9 +370,10 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo if record.EventType == .WINDOW_BUFFER_SIZE_EVENT { // Discard any additional resize event. while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { - was_resized = true; read_input(); } + + was_resized = true; return false; } @@ -382,13 +384,13 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo read_input(); count -= 1; } - + // When waiting indefinitely... just continue. if timeout_milliseconds < 0 then continue; // Either break due to timeout, or update the remaining timeout. now := current_time_monotonic(); - if now >= expiration then return false; + if now >= expiration then break; timeout_milliseconds = xx to_milliseconds(expiration - now); } @@ -400,7 +402,7 @@ OS_was_terminal_resized :: () -> bool { was_resized = true; read_input(); } - + defer was_resized = false; return was_resized; } |
