aboutsummaryrefslogtreecommitdiff
path: root/modules/TUI/windows.jai
diff options
context:
space:
mode:
Diffstat (limited to 'modules/TUI/windows.jai')
-rw-r--r--modules/TUI/windows.jai406
1 files changed, 406 insertions, 0 deletions
diff --git a/modules/TUI/windows.jai b/modules/TUI/windows.jai
new file mode 100644
index 0000000..f79a5cf
--- /dev/null
+++ b/modules/TUI/windows.jai
@@ -0,0 +1,406 @@
+#scope_file
+
+#import "Basic";
+#import "Atomics";
+#import "System";
+#import "Windows";
+
+ // https://learn.microsoft.com/windows/win32/winprog/windows-data-types
+ LPVOID :: *void;
+ BOOL :: bool;
+ CHAR :: s8;
+ WCHAR :: s16;
+ SHORT :: s16;
+ USHORT :: u16;
+ WORD :: u16;
+ DWORD :: s32;
+ LPDWORD :: *s32;
+ 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;
+
+ // 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;
+
+ // 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
+ PeekConsoleInputA :: (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 {
+ ENABLE_PROCESSED_INPUT; // If enable, control keys (Ctrl+C, Backspace, ...) are processed by the system.
+ 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_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; //
+ _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; //
+ ENABLE_WRAP_AT_EOL_OUTPUT; //
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING; //
+ DISABLE_NEWLINE_AUTO_RETURN; //
+ ENABLE_LVB_GRID_WORLDWIDE; //
+ _UNUSED_0020_;
+ _UNUSED_0040_;
+ _UNUSED_0080_;
+ }
+
+ COORD :: struct {
+ X : SHORT;
+ Y : SHORT;
+ }
+
+ SMALL_RECT :: struct {
+ Left : SHORT;
+ Top : SHORT;
+ Right : SHORT;
+ Bottom : SHORT;
+ }
+
+ CONSOLE_SCREEN_BUFFER_INFO :: struct {
+ dwSize : COORD;
+ dwCursorPosition : COORD;
+ wAttributes : WORD;
+ srWindow : SMALL_RECT;
+ dwMaximumWindowSize : COORD;
+ }
+
+ 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;
+ }
+
+ stdin: HANDLE;
+ initial_stdin_mode: u32;
+ raw_stdin_mode: Console_Input_Mode;
+
+ stdout: HANDLE;
+ initial_stdout_mode: u32;
+ raw_stdout_mode: Console_Output_Mode;
+
+
+////////////////////////////////////////////////////////////////////////////////
+// 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);
+ }
+}
+
+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 {
+ 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);
+ }
+ return record;
+}
+
+read_input :: inline () -> INPUT_RECORD {
+ 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);
+ }
+ return record;
+}
+
+count_input :: inline () -> s32 {
+ count: s32;
+ if GetNumberOfConsoleInputEvents(stdin, *count) == false {
+ _, error_message := get_error_value_and_string();
+ assert(false, error_message);
+ }
+ return count;
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+
+#scope_export
+
+OS_prepare_terminal :: () {
+
+ // stdin
+ stdin = GetStdHandle(STD_INPUT_HANDLE);
+ if stdin == INVALID_HANDLE_VALUE {
+ print("Invalid input handler.", to_standard_error = true);
+ return;
+ }
+ if xx GetConsoleMode(stdin, *initial_stdin_mode) == false {
+ print("Failed to get input mode.", to_standard_error = true);
+ 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);
+ return;
+ }
+
+ // stdout
+ stdout = GetStdHandle(STD_OUTPUT_HANDLE);
+ if stdout == INVALID_HANDLE_VALUE {
+ print("Invalid output handler.", to_standard_error = true);
+ return;
+ }
+ if xx GetConsoleMode(stdout, *initial_stdout_mode) == false {
+ print("Failed to get output mode.", to_standard_error = true);
+ 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);
+ return;
+ }
+}
+
+OS_reset_terminal :: () {
+ if xx SetConsoleMode(stdin, initial_stdin_mode) == false {
+ print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true);
+ return;
+ }
+ if xx SetConsoleMode(stdout, initial_stdout_mode) == false {
+ print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true);
+ return;
+ }
+}
+
+OS_flush_input :: inline () {
+ /* NOTE
+ 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?
+ }
+}
+
+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.");
+
+ bytes_read: s32 = 0;
+ available_inputs := count_input();
+
+ while bytes_to_read > 0 && available_inputs > 0 {
+ record := read_input();
+
+ if record.EventType == .WINDOW_BUFFER_SIZE_EVENT {
+ was_resized = true;
+ }
+
+ if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true {
+ buffer[bytes_read] = xx record.KeyEvent.AsciiChar;
+ bytes_to_read -= 1;
+ bytes_read += 1;
+ }
+ available_inputs -= 1;
+ }
+ return bytes_read;
+}
+
+// timeout_milliseconds
+// 0: do not wait
+// -1: wait indefinitely
+OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool {
+
+ /* TODO
+ Add a good comment explaining how the windows part of the module was implemented... what's the idea behind it.
+ Something like, Since windows provides a single input buffer with all events, we need to peek through them and
+ discard the ones that are of no use for us.
+ Because it's a single buffer, all functions need to do repeated work (see if it's resize, see if it's a key press)
+ ... and so on.
+ */
+
+ expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0);
+
+ // Possible values for poll_return TODO NOT BEING USED
+ WAIT_ABANDONED :: 0x00000080; // Mutex stuff.
+ 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);
+ }
+
+ // 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 {
+ was_resized = true;
+ read_input();
+ }
+ 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 return false;
+ timeout_milliseconds = xx to_milliseconds(expiration - now);
+ }
+
+ return false;
+}
+
+OS_was_terminal_resized :: () -> bool {
+ while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT {
+ was_resized = true;
+ read_input();
+ }
+
+ defer was_resized = false;
+ return was_resized;
+}