aboutsummaryrefslogtreecommitdiff
path: root/TUI/windows.jai
diff options
context:
space:
mode:
authordam <dam@gudinoff>2024-05-26 01:22:59 +0100
committerdam <dam@gudinoff>2024-08-29 10:03:35 +0100
commit7d358263c8b7dd154f2e7f049659f4c706ab5b75 (patch)
treed72ad3b62f25d8927b94ec16da4ebd095f5a8e85 /TUI/windows.jai
downloadjai-modules-7d358263c8b7dd154f2e7f049659f4c706ab5b75.tar.zst
jai-modules-7d358263c8b7dd154f2e7f049659f4c706ab5b75.zip
Saturation, TUI, and UTF8 version 1.0.
Diffstat (limited to 'TUI/windows.jai')
-rw-r--r--TUI/windows.jai390
1 files changed, 390 insertions, 0 deletions
diff --git a/TUI/windows.jai b/TUI/windows.jai
new file mode 100644
index 0000000..f8d8bc8
--- /dev/null
+++ b/TUI/windows.jai
@@ -0,0 +1,390 @@
+#scope_file
+
+#import "Basic";
+#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;
+ BOOL :: bool;
+ CHAR :: s8;
+ WCHAR :: s16;
+ SHORT :: s16;
+ WORD :: u16;
+ DWORD :: u32;
+ LPDWORD :: *u32;
+ UINT :: u32;
+ PINPUT_RECORD :: *INPUT_RECORD;
+
+ // 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 {
+ ENABLE_PROCESSED_INPUT; // If set, control sequences are processed by the system.
+ ENABLE_LINE_INPUT; // If set, ReadFile and ReadConsole function return on CR; otherwise return when characters are available.
+ ENABLE_ECHO_INPUT; // If set, Echoes input on screen. Only available if ENABLE_LINE_INPUT is set.
+ ENABLE_WINDOW_INPUT;
+ ENABLE_MOUSE_INPUT;
+ ENABLE_INSERT_MODE;
+ _UNUSED_0040_;
+ _UNUSED_0080_;
+ _UNUSED_0100_;
+ ENABLE_VIRTUAL_TERMINAL_INPUT; // If set, makes user input available to the ReadConsole function.
+ _UNUSED_0400_;
+ _UNUSED_0800_;
+ }
+
+ // https://learn.microsoft.com/en-us/windows/console/setconsolemode
+ // https://learn.microsoft.com/en-us/windows/console/high-level-console-modes
+ Console_Output_Mode :: enum_flags u32 {
+ ENABLE_PROCESSED_OUTPUT; // If set, ASCII control sequences are processed by the system.
+ ENABLE_WRAP_AT_EOL_OUTPUT; // If set, the cursor moves to the beginning of the next row when it reaches the end of the current row.
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING; // If set, VT100 control sequences are processed by the system.
+ DISABLE_NEWLINE_AUTO_RETURN;
+ ENABLE_LVB_GRID_WORLDWIDE;
+ _UNUSED_0020_;
+ _UNUSED_0040_;
+ _UNUSED_0080_;
+ }
+
+ COORD :: struct {
+ X : SHORT;
+ Y : SHORT;
+ }
+
+ INPUT_RECORD_EVENT_TYPE :: enum u16 {
+ KEY_EVENT :: 0x0001;
+ MOUSE_EVENT :: 0x0002;
+ WINDOW_BUFFER_SIZE_EVENT :: 0x0004;
+ MENU_EVENT :: 0x0008;
+ FOCUS_EVENT :: 0x0010;
+ }
+
+ INPUT_RECORD :: struct {
+ EventType : INPUT_RECORD_EVENT_TYPE;
+ union {
+ KeyEvent : KEY_EVENT_RECORD;
+ MouseEvent : MOUSE_EVENT_RECORD;
+ WindowBufferSizeEvent : WINDOW_BUFFER_SIZE_RECORD;
+ // These events are used internally and should be ignored.
+ MenuEvent : MENU_EVENT_RECORD;
+ FocusEvent : FOCUS_EVENT_RECORD;
+ }
+ }
+
+ KEY_EVENT_RECORD :: struct {
+ bKeyDown : BOOL;
+ wRepeatCount : WORD #align 4;
+ wVirtualKeyCode : WORD;
+ wVirtualScanCode : WORD;
+ union {
+ UnicodeChar : WCHAR;
+ AsciiChar : CHAR;
+ }
+ dwControlKeyState : DWORD;
+ }
+
+ MOUSE_EVENT_RECORD :: struct {
+ dwMousePosition : COORD;
+ dwButtonState : DWORD;
+ dwControlKeyState : DWORD;
+ dwEventFlags : DWORD;
+ }
+
+ WINDOW_BUFFER_SIZE_RECORD :: struct {
+ dwSize : COORD;
+ }
+
+ MENU_EVENT_RECORD :: struct {
+ dwCommandId : UINT;
+ }
+
+ FOCUS_EVENT_RECORD :: struct {
+ bSetFocus : BOOL;
+ }
+
+ // https://learn.microsoft.com/en-us/windows/console/readconsoleinput
+ ReadConsoleInputW :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> success: bool #foreign kernel32;
+
+ // https://learn.microsoft.com/windows/console/readconsole
+ 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;
+
+ // https://learn.microsoft.com/windows/console/getnumberofconsoleinputevents
+ GetNumberOfConsoleInputEvents :: (hConsoleInput: HANDLE, lpcNumberOfEvents: LPDWORD) -> bool #foreign kernel32;
+
+ // https://learn.microsoft.com/en-us/windows/console/peekconsoleinput
+ PeekConsoleInputW :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> bool #foreign kernel32;
+
+////////////////////////////////////////////////////////////////////////////////
+
+ stdin: HANDLE;
+ initial_stdin_mode: u32;
+ raw_stdin_mode: Console_Input_Mode;
+
+ stdout: HANDLE;
+ initial_stdout_mode: u32;
+ raw_stdout_mode: Console_Output_Mode;
+
+ was_resized: bool;
+
+ widechar_buffer: [512] u16;
+
+////////////////////////////////////////////////////////////////////////////////
+
+peek_input :: () -> INPUT_RECORD, success := true {
+ record: INPUT_RECORD;
+ records_read: u32;
+ if PeekConsoleInputW(stdin, *record, 1, *records_read) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to peek input: code %, %", error_code, error_string);
+ return record, false;
+ }
+ return record;
+}
+
+read_input :: () -> INPUT_RECORD, success := true {
+ record: INPUT_RECORD;
+ records_read: u32;
+ if ReadConsoleInputW(stdin, *record, 1, *records_read) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to read input: code %, %", error_code, error_string);
+ return record, false;
+ }
+ return record;
+}
+
+count_input :: () -> u32, success := true {
+ count: u32;
+ if GetNumberOfConsoleInputEvents(stdin, *count) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to count input: code %, %", error_code, error_string);
+ return 0, false;
+ }
+ return count;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#scope_module
+
+OS_prepare_terminal :: () -> success := true {
+
+ // stdin
+ stdin = GetStdHandle(STD_INPUT_HANDLE);
+ if stdin == INVALID_HANDLE_VALUE {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Invalid input handler: code %, %", error_code, error_string);
+ return false;
+ }
+ if xx GetConsoleMode(stdin, *initial_stdin_mode) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to get input mode: code %, %", error_code, error_string);
+ return false;
+ }
+ 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 xx SetConsoleMode(stdin, xx raw_stdin_mode) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to set input mode: code %, %", error_code, error_string);
+ return false;
+ }
+
+ // stdout
+ stdout = GetStdHandle(STD_OUTPUT_HANDLE);
+ if stdout == INVALID_HANDLE_VALUE {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Invalid output handler: code %, %", error_code, error_string);
+ return false;
+ }
+ if xx GetConsoleMode(stdout, *initial_stdout_mode) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to get output mode: code %, %", error_code, error_string);
+ return false;
+ }
+ 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 xx SetConsoleMode(stdout, xx raw_stdout_mode) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to set output mode: code %, %", error_code, error_string);
+ return false;
+ }
+
+ // 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);
+
+ return;
+}
+
+OS_reset_terminal :: () -> success := true {
+ if xx SetConsoleMode(stdin, initial_stdin_mode) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to reset input mode: code %, %", error_code, error_string);
+ return false;
+ }
+ if xx SetConsoleMode(stdout, initial_stdout_mode) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to reset output mode: code %, %", error_code, error_string);
+ return false;
+ }
+ return;
+}
+
+OS_flush_input :: () {
+ // 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.
+ if FlushConsoleInputBuffer(stdin) == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to flush input: code %, %", error_code, error_string);
+ }
+}
+
+OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success := true {
+
+ S32_MAX :: 0x7fff_ffff;
+ if (bytes_to_read > S32_MAX) {
+ log_tui_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 := 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 {
+
+ widechar_view: [] u16;
+ widechar_view.data = widechar_buffer.data;
+
+ chars_to_read := ifx available_inputs <= widechar_buffer.count then available_inputs else widechar_buffer.count;
+
+ success = ReadConsoleW(stdin, widechar_view.data, chars_to_read, *widechar_view.count, null);
+ if success == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_error("Failed to read console: code %, %", error_code, error_string);
+ return 0, false;
+ }
+
+ result:, success = wide_to_utf8_new(widechar_view.data, xx widechar_view.count);
+ if success == false {
+ error_code, error_string := get_error_value_and_string();
+ log_tui_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;
+
+ }
+
+ // Discard other input events.
+ case;
+ read_input();
+
+ }
+ available_inputs = count_input();
+ }
+ return bytes_read;
+}
+
+// timeout_milliseconds
+// 0: do not wait
+// -1: wait indefinitely
+OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool, success := true {
+ // The Windows API provides all input events (keyboard, mouse, window resize) on a single input buffer.
+ // To make it match this module's API, we need to do some pre-processing while waiting for input.
+ // This means that OS_wait_for_input will peek at the input events, signal if a window resize is found,
+ // and discard unwanted events (like button release events).
+ // A similar logic is applied in OS_read_input.
+
+ expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0);
+
+ // 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 {
+ 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_tui_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 {
+ record := peek_input();
+
+ if record.EventType == .WINDOW_BUFFER_SIZE_EVENT {
+ // Discard any additional resize event.
+ while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT {
+ read_input();
+ }
+
+ was_resized = true;
+ return false;
+ }
+
+ if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true {
+ return true;
+ }
+
+ 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 break;
+ timeout_milliseconds = xx to_milliseconds(expiration - now);
+ }
+
+ return false;
+}
+
+OS_was_terminal_resized :: inline () -> bool {
+ while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT {
+ was_resized = true;
+ read_input();
+ }
+
+ defer was_resized = false;
+ return was_resized;
+}