#if OS == { case .LINUX; #load "unix.jai"; case .MACOS; #load "unix.jai"; case .WINDOWS; #load "windows.jai"; case; #assert(false, "Unsupported OS."); } #import "Basic"; #import "String"; #import "Thread"; #load "key_map.jai"; // Special Graphics Characters Drawings :: struct { Blank :: "\x5F"; Diamond :: "\x60"; Checkerboard :: "\x61"; HorizontalTab :: "\x62"; FormFeed :: "\x63"; CarriageReturn :: "\x64"; LineFeed :: "\x65"; DegreeSymbol :: "\x66"; PlusMinus :: "\x67"; NewLine :: "\x68"; VerticalTab :: "\x69"; CornerBR :: "\x6A"; CornerTR :: "\x6B"; CornerTL :: "\x6C"; CornerBL :: "\x6D"; Cross :: "\x6E"; LineHT :: "\x6F"; LineHt :: "\x70"; LineH :: "\x71"; LineHb :: "\x72"; LineHB :: "\x73"; TeeL :: "\x74"; TeeR :: "\x75"; TeeB :: "\x76"; TeeT :: "\x77"; LineV :: "\x78"; LessThanOrEqual :: "\x79"; GreaterThanOrEqual :: "\x7A"; Pi :: "\x7B"; NotEqual :: "\x7C"; PoundSign :: "\x7D"; CenteredDot :: "\x7E"; } Commands :: struct { AlternateScreenBuffer :: "\e[?1049h"; MainScreenBuffer :: "\e[?1049l"; DrawingMode :: "\e(0"; TextMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; ClearScrollBack :: "\e[3J"; Bell :: "\x07"; SetWindowTitle :: "\e]0;%\e\\"; RefreshWindow :: "\e[7t"; // TODO Not yet tested. SetIEC2022 :: "\e%@"; SetUTF8 :: "\e%G"; SetGraphicsRendition :: "\e[%m"; // 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. } clear_style :: () { write_string(#run sprint(Commands.SetGraphicsRendition, "0")); } Colors8b :: struct { Black :: 0; Maroon :: 1; Green :: 2; Olive :: 3; Navy :: 4; Purple :: 5; Teal :: 6; Silver :: 7; Gray :: 8; Red :: 9; Lime :: 10; Yellow :: 11; Blue :: 12; Magenta :: 13; Cyan :: 14; White :: 15; } // Fix color tables using this: https://devmemo.io/cheatsheets/terminal_escape_code/ // TODO Maybe rename. set_style_colors :: (foreground: u8, background: u8) { print( #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), foreground, background); } // set_colors_24b :: (foreground_r: u255) // TODO https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, negative: bool = false) { print( #run sprint(Commands.SetGraphicsRendition, "%;%;%;%"), ifx bold then 1 else 22, ifx underline then 4 else 24, ifx strike_through then 9 else 29, ifx negative then 7 else 27); } // 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. 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; // Terminal key-codes have 1 to 6 bytes so we'll use 8 bytes. KEY_SIZE :: #run type_info(Key).runtime_size; 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 { // TODO FIXME TEMPORARY MEMORY 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; } is_escape_code :: inline (key: Key) -> bool { /* TODO Check if LSB is not # but there is a `#`, then it's a escape code... or NONE... or RESIZE :S Or...we could change the special codes and set the `#` at the end... then we could simply do: return (key && 0x00FF) ^ # == 0 && (key && 0xFF00) == 0 */ result := false; while key != 0 { key >>= 8; result |= ((key ^ #char "#") == 0); } return result; } Keys :: struct #type_info_none { None : Key : #run to_key("#none"); Resize : Key : #run to_key("#resize"); Space : Key : #char " "; Enter : Key : #char "\r"; Tab : Key : #char "\t"; Escape : Key : 0x00000000_0000001B; Backspace : Key : 0x00000000_0000007F; Pause : Key : 0x00000000_0000001A; Up : Key : #run to_key("#up"); Down : Key : #run to_key("#down"); Right : Key : #run to_key("right"); Left : Key : #run to_key("left"); Home : Key : #run to_key("#home"); End : Key : #run to_key("#end"); Insert : Key : #run to_key("#ins"); Delete : Key : #run to_key("#del"); PgUp : Key : #run to_key("#pup"); PgDown : Key : #run to_key("#pdown"); F1 : Key : #run to_key("#f1"); F2 : Key : #run to_key("#f2"); F3 : Key : #run to_key("#f3"); F4 : Key : #run to_key("#f4"); F5 : Key : #run to_key("#f5"); F6 : Key : #run to_key("#f6"); F7 : Key : #run to_key("#f7"); F8 : Key : #run to_key("#f8"); F9 : Key : #run to_key("#f9"); F10 : Key : #run to_key("#f10"); F11 : Key : #run to_key("#f11"); F12 : Key : #run to_key("#f12"); } 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 { // 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_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; 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); } if OS_was_terminal_resized() return xx Keys.Resize; 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 return Keys.None; // By default, parse a single UTF8 character (1 to 4 bytes). to_parse := input_string; to_parse.count = count_utf8_bytes(input_string[0]); defer advance(*input_string, to_parse.count); // Advance over parsed input. // Try to parse escape code. if input_string[0] == #char "\e" && input_string.count > 1 { // Limit number of chars to parse. to_parse.count = ifx input_string.count > KEY_SIZE then KEY_SIZE else input_string.count; // Search for the longest escape code. key, success := table_find(*key_map, to_parse); while success == false && to_parse.count > 1 { to_parse.count -= 1; key, success = table_find(*key_map, to_parse); } if success { return key; // Escape code found, return it. } else { to_parse.count = 1; // No escape code found, return a single (escape) character. } } return to_key(to_parse); } // TODO Review me! read_input :: (count_limit: int = -1, terminators: .. u8) -> string { assert_is_initialized(); assert(count_limit >= 0 || terminators.count > 0, "Infinite loop detected, aborting."); // TODO Maybe just return!? if count_limit < 0 { builder: String_Builder; init_string_builder(*builder); while read_loop := true { buffer := get_current_buffer(*builder); buffer_data := get_buffer_data(buffer); previous_count := buffer.count; buffer.count += OS_read_input(buffer_data + buffer.count, buffer.allocated - buffer.count); for previous_count..buffer.count-1 { for t: terminators { if buffer_data[it] == t then break read_loop; } } if buffer.count == buffer.allocated then expand(*builder); OS_wait_for_input(); } return builder_to_string(*builder); } else { buffer := alloc_string(count_limit); buffer.count = 0; while read_loop := true { previous_count := buffer.count; buffer.count += OS_read_input(buffer.data + buffer.count, count_limit - buffer.count); if buffer.count == count_limit then break; for previous_count..buffer.count-1 { for t: terminators { if buffer[it] == t then break read_loop; } } OS_wait_for_input(); } return buffer; } } // TODO UNTESTED read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { assert(count_limit >= 0, "Invalid value on count_limit parameter."); str := alloc_string(count_limit); // TODO Show blinking cursor write_strings(Commands.StartBlinking, Commands.BlinkingUnderlineShape); row, col := get_cursor_position(); idx := 0; key := Keys.None; // TODO Some of these may be nice to have: // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line while key != Keys.Resize && key != Keys.Escape { // TODO How to alloc/release temporary memory here? key = get_key(); if key == { case Keys.Enter; break; case Keys.Left; if idx == 0 continue; idx -= 1; case Keys.Right; if idx == str.count-1 continue; idx += 1; case Keys.Delete; if idx == str.count-1 continue; for idx..str.count-2 { str.data[it] = str.data[it+1]; } case Keys.Backspace; if idx == 0 continue; idx -= 1; for idx..str.count-2 { str.data[it] = str.data[it+1]; } case; if is_escape_code(key) continue; // TODO NOT WORKING FIX THIS key_str := to_string(key); str.data[idx] = key_str.data[0]; idx += 1; } // append(*builder, str); // set_cursor_position(row, col); // write_builder(*builder, false); set_cursor_position(row, col+idx); for idx..count_limit print_character(#char " "); set_cursor_position(row, col); write_string(str); // print(">%<", builder_to_string(*builder,, temporary_allocator)); set_cursor_position(row, col+idx); } write_strings(Commands.StopBlinking, Commands.DefaultShape); /* Use the get_key to read user input and show it on screen... should allow to move the cursor left and right and to delete/backspace. Enter should end the input, returning the input string and the Enter key. Escape should discard the input returning an empty string and a None key. Resize should discard the input returning an empty string and a Resize key. */ // result := ifx key == Keys.Enter then builder_to_string(*builder) else ""; result := ifx key == Keys.Enter then str else ""; return result, key; } start :: () { if initialized == true return; setup_key_map(); // TODO This is being called multiple times... please fix me! 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, Commands.CursorNormalMode, Commands.KeypadNumMode); 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(); input_string.data = input_buffer.data; input_string.count = 0; } // TODO move style related procedures here! // 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(); auto_release_temp(); flush_input(); write_string(Commands.QueryWindowSizeInChars); rows, columns: int = ---; if OS_wait_for_input(0) { // Expected response format: \e[8;;t // where is the number of rows and of columns. FORMAT :: "\e[8;;t"; input := read_input(64, #char "t",, temporary_allocator); // 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, ";",, temporary_allocator); rows = parse_int(*parts[1]); columns = parse_int(*parts[2]); } // Some systems don't allow to query the terminal size directly. // In such cases, measure it indirectly by the maximum possible cursor position. else { flush_input(); cursor_row, cursor_column := get_cursor_position(); defer set_cursor_position(cursor_row, cursor_column); set_cursor_position(0xFFFF, 0xFFFF); rows, columns = get_cursor_position(); } return rows, columns; } set_cursor_position :: (row: int, column: int) { assert_is_initialized(); auto_release_temp(); print(Commands.SetCursorPosition, row, column); } get_cursor_position :: () -> row: int, column: int { assert_is_initialized(); auto_release_temp(); flush_input(); write_string(Commands.QueryCursorPosition); // Expected response format: \e[;R // where is the number of rows and of columns. FORMAT :: "\e[;R"; input := read_input(64, #char "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, ";",, temporary_allocator); 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! }