diff options
| -rw-r--r-- | TUI/module.jai | 228 | ||||
| -rw-r--r-- | TUI/unix.jai | 1 | ||||
| -rw-r--r-- | TUI/windows.jai | 180 | ||||
| -rw-r--r-- | ttt.jai | 23 |
4 files changed, 244 insertions, 188 deletions
diff --git a/TUI/module.jai b/TUI/module.jai index de56ee8..64567fa 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -1,9 +1,13 @@ -#if OS == .WINDOWS { - #load "windows.jai"; -} else #if (OS == .LINUX) || (OS == .MACOS) { - #load "unix.jai"; -} else { - #assert(false, "Unsupported OS."); +// TODO Move TUI into ./modules/TUI so we can stop calling --import_dir on compile. +#if OS == { + case .LINUX; + #load "unix.jai"; + case .MACOS; + #load "unix.jai"; + case .WINDOWS; + #load "windows.jai"; + case; + #assert(false, "Unsupported OS."); } #import "Basic"; @@ -61,7 +65,8 @@ Commands :: struct { RefreshWindow :: "\e[7t"; // TODO Not yet tested. - SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE + SetIEC2022 :: "\e%@"; + SetUTF8 :: "\e%G"; SetGraphicsRendition :: "\e[%m"; @@ -139,6 +144,7 @@ set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, } // TODO Maybe make the OS_* procedures as inline?! +// TODO Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. /* We wanted the Key type to represent either UTF-8 encoded characters and also keyboard keys. @@ -369,57 +375,13 @@ is_escape_code :: inline (key: Key) -> bool { return result; } -// TODO FIXME DEBUG HACK -test_union :: () -{ - // ti := type_info(K); - print("\n---\n%\n---\n", type_info(Key).*); - print("\n---\n%\n---\n", type_info(Key).runtime_size); - a: Key; - b: u8; - print(">%\n", a == b); - print(">%\n", a != Keys.None); - - c: string = ""; - print(">%\n", a == to_key(c)); - - d: []u8 = xx ""; - print(">%\n", a == to_key(d)); - - top := "┌───┐"; - btm := "└───┘"; - print(">%<\n", top); - print(">%<\n", btm); - - str := "abcd"; - tok := to_key(str); - tos := to_string(tok); - - print("1:%:\n", str); - print("2:"); - for 0..7 { - val := tok & 0xFF; - tok >>=8 ; - print("% ", FormatInt.{value=val, base=16, minimum_digits=2}); - } - print(":\n"); - print("3:%:\n", tos); - -} -#run test_union(); - -// Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. - - initialized := false; -// input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! +//input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! input_buffer : [8] u8; // TODO FIXME Input buffer is too small!!! input_string : string; input_override : Key; -was_resized : bool; - #run { // TODO FIXME DEBUG HACK or maybe... let it be?! // Some tests. @@ -437,6 +399,16 @@ set_next_key :: (key: Key) { get_key :: (timeout_milliseconds: s32 = -1) -> Key { assert_is_initialized(); + + /* + TODO + get_key already deals with utf8 codes, but we don't know when we're receiving ANSI escape codes. If initial key is escape and other keys are awaiting in the input buffer, we need to parse them as escaped sequences. See wikiedia* for help on that. Lets use the escape sequences used on windows amd forget all others. Those should be the most used ones; at least they are the cross-platform compatible ones :P + * https://en.m.wikipedia.org/wiki/ANSI_escape_code + + Check this + https://devmemo.io/cheatsheets/terminal_escape_code/ + */ + // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte is_utf8_continuation_byte :: inline (byte: u8) -> bool { @@ -457,105 +429,57 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { defer input_override = xx Keys.None; return input_override; } - + if OS_was_terminal_resized() return xx Keys.Resize; - // FIXME Old version... - // if input_string.count > 0 { - // defer advance(*input_string, 1); - // return input_string[0]; - // } - // FIXME New version... - if input_string.count > 0 #no_abc { // TODO Some trickery requires no_abc... which is not that nice in this case... - utf8_bytes := count_utf8_bytes(input_string[0]); - - // TODO We're assuming the terminal buffer contains the entirety of what we need to read for the UTF8 symbols. - if utf8_bytes > input_string.count { + should_read_input := false; + is_input_available := false; + + if input_string.count == 0 { + should_read_input = true; + is_input_available = OS_wait_for_input(timeout_milliseconds); + } + else if input_string.count < KEY_SIZE { + should_read_input = true; + is_input_available = OS_wait_for_input(0); + } - diff := utf8_bytes - input_string.count; + if OS_was_terminal_resized() return xx Keys.Resize; - // TODO Test this and make sure it's working...drop the following lines using Ctrl+V to fill the terminal buffer at once: - // d€€€a - // 1234567890123456789012345678901234567890 - print("<WoW>"); - - // Copy buffered bytes to the start, and read the remaining ones from input. - for 0..input_string.count-1 { - input_buffer[it] = input_string[it]; - } - aaa := OS_read_input(input_buffer.data + input_string.count, utf8_bytes-input_string.count); // TODO Does not check for read errors. - assert(aaa == diff, "READ MORE THAN EXPECTED"); - input_string.data = input_buffer.data; - input_string.count = utf8_bytes; + if should_read_input && is_input_available { + // Copy buffered bytes to the start, and read the remaining ones from input. + for 0..input_string.count-1 { + input_buffer[it] = input_string[it]; } + // Read input into remaining part of buffer. + bytes_read := OS_read_input(input_buffer.data + input_string.count, input_buffer.count - input_string.count); + input_string.data = input_buffer.data; + input_string.count += bytes_read; + } + + if input_string.count > 0 + { + utf8_bytes := count_utf8_bytes(input_string[0]); to_parse := input_string; to_parse.count = utf8_bytes; + + // Must be a terminal escape sequence. + if utf8_bytes == 1 && input_string[0] == #char "\e" { + assert(input_string.count <= KEY_SIZE, "Received oversized terminal sequence."); // TODO + to_parse.count = ifx input_string.count > KEY_SIZE then KEY_SIZE else input_string.count; // TODO We should look into the input_string and search for the following escape sequence or somehting!? + } + key := to_key(to_parse); - - advance(*input_string, utf8_bytes); + advance(*input_string, to_parse.count); return key; } - is_input_available := OS_wait_for_input(timeout_milliseconds); - - if OS_was_terminal_resized() return xx Keys.Resize; - - // FIXME Old version... - // if is_input_available { - // bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. - // if bytes_read > 0 { - // input_string.data = input_buffer.data; - // input_string.count = bytes_read; - // defer advance(*input_string, 1); - // return input_string[0]; - // } + // TODO try_parse_escape_code + // { + // assert(false, "TODO try_parse_escape_code"); // } - // FIXME New version... - if is_input_available { - bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. - if bytes_read > 0 { - input_string.data = input_buffer.data; - input_string.count = bytes_read; - utf8_bytes := count_utf8_bytes(input_string[0]); - - assert(utf8_bytes <= input_string.count, "The input buffer is too small."); // TODO Improve error message. - - // TODO This is only being done after the OS_wait_for_input... for now! - to_parse := input_string; - to_parse.count = utf8_bytes; - - // Must be a terminal escape sequence. - if utf8_bytes == 1 && input_string[0] == #char "\e" { - assert(input_string.count <= KEY_SIZE, "Received oversized terminal sequence."); // TODO - to_parse.count = ifx input_string.count > KEY_SIZE then KEY_SIZE else input_string.count; // TODO We should look into the input_string and search for the following escape sequence or somehting!? - } - - key := to_key(to_parse); - advance(*input_string, to_parse.count); - return key; - - /// /// /// /// /// /// /// /// /// - // DEBUG - // br, bc := get_cursor_position(); - // column := 3; - // row += 1; - // nr, rc := get_terminal_size(); - // - // if row >= (rc - 5) then row = 5; - // - // set_cursor_position(row, column); - // for 0..input_string.count-1 { - // print("%:% | ", - // FormatInt.{base= 2, minimum_digits = 8, value = input_string[it]}, - // FormatInt.{base= 16, minimum_digits = 2, value = input_string[it]}, - // ); // TODO DEBUG - // } - // set_cursor_position(br, bc); - /// /// /// /// /// /// /// /// /// - } - } return xx Keys.None; } @@ -696,6 +620,7 @@ start :: () { Commands.SetUTF8, Commands.CursorNormalMode, Commands.KeypadNumMode); + OS_prepare_terminal(); initialized = true; @@ -809,12 +734,11 @@ get_terminal_size :: () -> rows: int, columns: int { // Some systems don't allow to query the terminal size directly. // In such cases, measure it indirectly by the maximum possible cursor position. else { - #import "Math"; // TODO Maybe use S16_MAX values directly. flush_input(); cursor_row, cursor_column := get_cursor_position(); defer set_cursor_position(cursor_row, cursor_column); - set_cursor_position(S16_MAX, S16_MAX); + set_cursor_position(0xFFFF, 0xFFFF); rows, columns = get_cursor_position(); } @@ -824,8 +748,7 @@ get_terminal_size :: () -> rows: int, columns: int { set_cursor_position :: (row: int, column: int) { assert_is_initialized(); auto_release_temp(); - tmp_string := tprint(Commands.SetCursorPosition, row, column); - write_string(tmp_string); + print(Commands.SetCursorPosition, row, column); } get_cursor_position :: () -> row: int, column: int { @@ -868,29 +791,6 @@ set_terminal_title :: (title: string) { } -test :: () { - - // A) Testing stuff -#if true { - start(); - flush_input(); - set_cursor_position(S16_MAX, S16_MAX); - r_a, c_a := get_cursor_position(); - stop(); - print("\n\rA) size: %, %\n", r_a, c_a); -} - - // B) Built way -#if true { - start(); - r_b, c_b := get_terminal_size(); - stop(); - print("\n\rB) size: %, %\n", r_b, c_b); -} - -} - - #if OS == .WINDOWS { // Prototyping zone... keep clear! } diff --git a/TUI/unix.jai b/TUI/unix.jai index 2da4437..861fe11 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -204,6 +204,7 @@ //////////////////////////////////////////////////////////////////////////////// // Resize detection +was_resized : bool; resize_handler :: (signal_code : s32) #c_call { new_context : Context; diff --git a/TUI/windows.jai b/TUI/windows.jai index f704262..ff4d6a1 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -1,16 +1,23 @@ #scope_file +#import "Basic"; #import "Atomics"; #import "System"; #import "Windows"; // https://learn.microsoft.com/windows/win32/winprog/windows-data-types LPVOID :: *void; - LPDWORD :: *s32; BOOL :: bool; + CHAR :: s8; + WCHAR :: s16; SHORT :: s16; + USHORT :: u16; WORD :: u16; DWORD :: s32; + LPDWORD :: *s32; + + + 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 @@ -18,11 +25,16 @@ 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) -> bool #foreign kernel32; + 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; @@ -33,8 +45,17 @@ // 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-waitforsingleobjectex - WaitForSingleObjectEx :: (hHandle: HANDLE, dwMilliseconds: DWORD, bAlertable: BOOL) -> 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 @@ -42,7 +63,7 @@ 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. - _UNUSED_0008_; + 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_; @@ -86,6 +107,49 @@ dwMaximumWindowSize : COORD; } + 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; + } + //Event : EventUnion; + } + + INPUT_RECORD_EVENT_TYPE :: enum s32 { + KEY_EVENT :: 0x0001; + MOUSE_EVENT :: 0x0002; + WINDOW_BUFFER_SIZE_EVENT :: 0x0004; + MENU_EVENT :: 0x0008; + FOCUS_EVENT :: 0x0010; + } + + KEY_EVENT_RECORD :: struct { + bKeyDown : BOOL; + wRepeatCount : WORD; + 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; + } stdin: HANDLE; initial_stdin_mode: u32; @@ -98,6 +162,7 @@ //////////////////////////////////////////////////////////////////////////////// // Resize detection +was_resized : bool; resize_handler :: (signal_code : s32) #c_call { /* TODO @@ -131,6 +196,36 @@ restore_resize_handler :: () { */ } +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 @@ -187,44 +282,101 @@ OS_reset_terminal :: () { } OS_flush_input :: inline () { - // TODO - https://learn.microsoft.com/en-us/windows/console/flushconsoleinputbuffer - // BOOL WINAPI FlushConsoleInputBuffer(_In_ HANDLE hConsoleInput); /* 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; success := ReadConsoleA(stdin, buffer, cast(s32)bytes_to_read, *bytes_read); if success == false { _, error_message := get_error_value_and_string(); return -1, true, error_message; } + // print(">%:%<", bytes_to_read, bytes_read); TODO DEBUG return bytes_read; } -OS_wait_for_input :: (timeout_milliseconds: s32) -> is_input_available: bool { +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { /* TODO Try to implement using: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobjectex + + This wait procedure on windows should check if next input is valid (keyboard down) before returning, otherwise it should discar that input and go back to wait (if more sleep is allowed). */ - poll_return := WaitForSingleObjectEx(stdin, timeout_milliseconds, true); - error_code, error_message := get_error_value_and_string(); // FIXME Not used. + expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0); + // Possible values for poll_return TODO NOT BEING USED WAIT_ABANDONED :: 0x00000080; - WAIT_IO_COMPLETION :: 0x000000C0; + //WAIT_IO_COMPLETION :: 0x000000C0; WAIT_OBJECT_0 :: 0x00000000; WAIT_TIMEOUT :: 0x00000102; WAIT_FAILED :: 0xFFFFFFFF; - - return ifx poll_return == 0 then true else false; + //return ifx poll_return == WAIT_OBJECT_0 then true else false; + + 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); + } +// if poll_return != WAIT_OBJECT_0 then return false; + + // Discard invalid input until a valid input is found or no input is left. + count := count_input(); + while count > 0 { + record := peek_input(); + if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true || record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + return true; + } + read_input(); // TODO Discard input. + count -= 1; + } + + now := current_time_monotonic(); + if now >= expiration then return false; + timeout_milliseconds = xx to_milliseconds(expiration - now); + } + + return false; } OS_was_terminal_resized :: () -> bool { - return atomic_swap(*was_resized, false); // TODO If the windows implementation is similar, we may push this into the main module file. + + defer was_resized = false; // TODO Not using this flag... what happens if ... not sure what... :thinking: + + flag: = false; + + record := peek_input(); + while record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + read_input(); + record = peek_input(); + flag = true; + } + + //return was_resized; + return flag; } @@ -1237,13 +1237,6 @@ main :: () { // -- -- -- Testing TUI -- START - // TODO Test input - - { - TUI.test(); - exit(0); - } - if 1 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); @@ -1268,8 +1261,16 @@ main :: () { while(key != #char "q") { __mark := get_temporary_storage_mark(); key = TUI.get_key(1000); - if key >= 32 && key <= 128 then print_character(cast,force(u8)key); - else write_string("-"); + if key == TUI.Keys.None { + write_string("-"); + } + else if key == TUI.Keys.Resize { + write_string("#"); + } + else { + // else if key >= 32 && key <= 128 then print_character(cast,force(u8)key) + write_string(TUI.to_string(key)); + } set_temporary_storage_mark(__mark); } TUI.stop(); @@ -1307,7 +1308,7 @@ main :: () { print("> success\n", to_standard_error = true); } - #if 1 { + if 1 { print("TEST : print keys and set terminal title --\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); @@ -1400,6 +1401,8 @@ main :: () { TUI.stop(); } + write_string("DONE"); + exit(0); // -- -- -- Testing TUI -- STOP |
