#if OS == .WINDOWS { #load "windows.jai"; } else #if (OS == .LINUX) || (OS == .MACOS) { #load "unix.jai"; } else { #assert(false, "Unsupported OS."); } #import "Basic"; #import "String"; #import "Thread"; Drawings :: struct { // TODO Maybe just use unicode instead of jumping back and forth between drawing mode?! // test_drawing_without_mode :: () { // top := "┌───┐"; // btm := "└───┘"; // print(">%<\n", top); // print(">%<\n", btm); // } // #run test_drawing_without_mode(); CornerBR :: "\x6A"; CornerTR :: "\x6B"; CornerTL :: "\x6C"; CornerBL :: "\x6D"; Cross :: "\x6E"; LineH :: "\x71"; TeeL :: "\x74"; TeeR :: "\x75"; TeeB :: "\x76"; TeeT :: "\x77"; LineV :: "\x78"; Blank :: "\x5F"; Diamond :: "\x60"; Checkerboard :: "\x61"; PlusMinus :: "\x67"; LessThanOrEqual :: "\x79"; GreaterThanOrEqual :: "\x7A"; Pi :: "\x7B"; NotEqual :: "\x7C"; CenteredDot :: "\x7E"; } Commands :: struct { AlternateScreenBuffer :: "\e[?1049h"; MainScreenBuffer :: "\e[?1049l"; DrawingMode :: "\e(0"; TextMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; Bell :: "\x07"; SetWindowTitle :: "\e]0;%\x07"; RefreshWindow :: "\e[7t"; // TODO Not yet tested. SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE // WIP WIP WIP // Add commands to change StyleNormal/Italic/Bold/Color/WhatNot! // Cursor Position SetCursorPosition :: "\e[%;%H"; // Cursor Visibility ShowCursor :: "\e[?25h"; HideCursor :: "\e[?25l"; StartBlinking :: "\e[?25h]"; StopBlinking :: "\e[?25l]"; SaveCursorPosition :: "\e7"; RestoreCursorPosition :: "\e8"; // Cursor Shape DefaultShape :: "\e[0 q"; BlinkingBlockShape :: "\e[1 q"; SteadyBlockShape :: "\e[2 q"; BlinkingUnderlineShape :: "\e[3 q"; SteadyUnderlineShape :: "\e[4 q"; BlinkingBarShape :: "\e[5 q"; SteadyBarShape :: "\e[6 q"; // Input Mode KeypadAppMode :: "\e="; KeypadNumMode :: "\e>"; CursorAppMode :: "\e[?1h"; CursorNormalMode :: "\e[?1l"; // Query State QueryCursorPosition :: "\e[6n"; // Emits the cursor position as: "ESC [ ; R" Where = row and = column. QueryDeviceAttributes :: "\e[0c"; QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. } GraphicsStyle :: struct { BackgroundColor : u8; ForegroundColor : u8; Bold : bool; Italic : bool; Underline : bool; Blinking : bool; Inverse : bool; StrikeThrough : bool; } // TODO Maybe make the OS_* procedures as inline?! /* We wanted the Key type to represent either UTF-8 encoded characters and also keyboard keys. The UTF-8 only requires up to 4 bytes, but some keyboard keys return up to 6 bytes. Therefore, we rounded it up to 8 bytes to support all this and more (if needed). This has to be compatible with: (#char "a" == key) ... so "a" must be stored in the LSB of key |-|-|-|-|-| string |a|b|c|0|0| key/u64 |0|0|c|b|a| -> that in memory lays as (BE:|0|0|c|b|a|) and (LE:|a|b|c|0|0|) */ Key :: u64; KEY_SIZE :: #run type_info(Key).runtime_size; Keys :: struct { // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. None : Key : 0xF0000000_0000000F; Resize : Key : 0xF0000000_0000001F; Up : Key : #run to_key("\e[A"); Down : Key : #run to_key("\e[B"); Right : Key : #run to_key("\e[C"); Left : Key : #run to_key("\e[D"); PgUp : Key : #run to_key("\e[5~"); PgDown : Key : #run to_key("\e[6~"); } to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { k: Key; // #if DEBUG { // assert(str.count <= 8); // TODO Add DEBUG to module parameters. // } for 0..str.count-1 #no_abc { k |= ((cast(u64)str[it]) << (it*8)); } return k; } to_string :: inline (key: Key) -> string { str := talloc_string(KEY_SIZE); str.count = 0; while key != 0 #no_abc { str[str.count] = xx key & 0xFF; key >>= 8; str.count += 1; } return str; } 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 : [8] u8; // TODO FIXME Input buffer is too small!!! input_string : string; input_override : Key; #run { // Some tests. assert(input_buffer.count >= 6, "The input buffer size must be capable to hold an entire terminal (6 bytes) or UTF8 (4 bytes) code."); } assert_is_initialized :: inline () { assert(initialized, "TUI is not ready."); } set_next_key :: (key: Key) { assert_is_initialized(); input_override = key; } get_key :: (timeout_milliseconds: s32 = -1) -> Key { assert_is_initialized(); // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte is_utf8_continuation_byte :: inline (byte: u8) -> bool { return (byte & 0xC0) == 0x80; } // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte count_utf8_bytes :: inline (byte: u8) -> int { if (byte & 0xE0) == 0xC0 return 1+1; if (byte & 0xF0) == 0xE0 return 1+2; if (byte & 0xF8) == 0xF0 return 1+3; return 1; } if input_override != xx Keys.None { 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 { diff := utf8_bytes - input_string.count; // 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(""); // 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; } to_parse := input_string; to_parse.count = utf8_bytes; key := to_key(to_parse); advance(*input_string, utf8_bytes); 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]; // } // } // 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" { to_parse.count = ifx input_string.count > 6 then 6 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; } get_str :: (count_limit: int = -1, allocator: Allocator = temp) -> string { assert_is_initialized(); assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); if count_limit < 0 { builder: String_Builder(); builder.allocator = allocator; init_string_builder(*builder); while(1) { buffer := get_current_buffer(*builder); buffer_data := get_buffer_data(buffer); buffer.count = OS_read_input(buffer_data, buffer.allocated); // TODO Does not check for read errors. if buffer.count < buffer.allocated break; expand(*builder); } return builder_to_string(*builder, allocator); } else { buffer := alloc_string(count_limit, allocator); buffer.count = OS_read_input(buffer.data, count_limit); return buffer; } } start :: () { if initialized == true return; input_string.data = input_buffer.data; input_string.count = 0; input_override = xx Keys.None; write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.AlternateScreenBuffer, Commands.SetUTF8); OS_prepare_terminal(); initialized = true; } stop :: () { if initialized == false return; initialized = false; OS_reset_terminal(); write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } flush_input :: () { OS_flush_input(); } // WIP WIP WIP // set_style :: () { // print("", Commands.) -- // } draw_box :: (x: int, y: int, width: int, height: int) { assert_is_initialized(); // TODO Check if using a String_Builder improves performance (measure it)! // TODO Validate input parameters against the terminal size. assert(x > 0 && y > 0 && width > 1 && height > 1, "Invalid arguments."); auto_release_temp(); tmp_string: string; tmp_string = tprint(Commands.SetCursorPosition, y, x); write_strings( Commands.DrawingMode, tmp_string, Drawings.CornerTL ); for 1..width-2 { write_string(Drawings.LineH); } write_string(Drawings.CornerTR); for idx: y+1..y+height-2 { tmpL := tprint(Commands.SetCursorPosition, idx, x); tmpR := tprint(Commands.SetCursorPosition, idx, x+width-1); write_strings( tmpL, Drawings.LineV, tmpR, Drawings.LineV); } tmpBL := tprint(Commands.SetCursorPosition, y+height-1, x); write_strings( tmpBL, Drawings.CornerBL); for 1..width-2 { write_string(Drawings.LineH); } write_string(Drawings.CornerBR); write_string(Commands.TextMode); } // TODO Maybe rename to "clear()" clear_terminal :: inline () { assert_is_initialized(); write_string(Commands.ClearScreen); } // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { assert_is_initialized(); rows, columns: int = ---; #if OS == .WINDOWS { rows, columns = OS_get_terminal_size(); } else { auto_release_temp(); flush_input(); write_string(Commands.QueryWindowSizeInChars); input := get_str(64); // Expected response format: \e[8;;t // where is the number of rows and of columns. FORMAT :: "\e[8;;t"; // Discard head noise. while input.count >= 3 && (input[0] != FORMAT[0] || input[1] != FORMAT[1] || input[2] != FORMAT[2]) { advance(*input); } // Discard tail noise. while input.count >= 3 && input[input.count-1] != FORMAT[FORMAT.count-1] { input.count -= 1; } assert(input.count >= 3 && input[0] == FORMAT[0] && input[1] == FORMAT[1] && input[2] == FORMAT[2] && input[input.count-1] == FORMAT[FORMAT.count-1], "Query window size in chars returned invalid response."); parts := split(input, ";"); rows = parse_int(*parts[1]); columns = parse_int(*parts[2]); } return rows, columns; } set_cursor_position :: (row: int, column: int) { assert_is_initialized(); auto_release_temp(); tmp_string := tprint(Commands.SetCursorPosition, row, column); write_string(tmp_string); } get_cursor_position :: () -> row: int, column: int { assert_is_initialized(); auto_release_temp(); flush_input(); write_string(Commands.QueryCursorPosition); input := get_str(64); // Expected response format: \e[;R // where is the number of rows and of columns. FORMAT :: "\e[;R"; // Discard head noise. while input.count >= 2 && (input[0] != FORMAT[0] || input[1] != FORMAT[1]) { advance(*input); } // Discard tail noise. while input.count >= 2 && input[input.count-1] != FORMAT[FORMAT.count-1] { input.count -= 1; } assert(input.count >= 2 && input[0] == FORMAT[0] && input[1] == FORMAT[1] && input[input.count-1] == FORMAT[FORMAT.count-1], "Query cursor position returned invalid response."); advance(*input, 2); parts := split(input, ";"); row := parse_int(*parts[0]); column := parse_int(*parts[1]); return row, column; } set_terminal_title :: (title: string) { assert_is_initialized(); print(Commands.SetWindowTitle, title); } #if OS == .WINDOWS { // Prototyping zone... keep clear! } else #if OS == .LINUX || OS == .MACOS { // Prototyping zone... keep clear! }