#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; }