aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/TUI/module.jai23
-rw-r--r--modules/TUI/windows.jai266
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;
}