#module_parameters(COLOR_BIT_DEPTH := 24); #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. } #if COLOR_BIT_DEPTH == 4 { #load "palette_4b.jai"; set_colors :: inline (foreground: Palette, background: Palette) { print( #run sprint("% %", Commands.SetGraphicsRendition, Commands.SetGraphicsRendition), cast(u8)foreground + 30, cast(u8)background + 40); } } else #if COLOR_BIT_DEPTH == 8 { #load "palette_8b.jai"; set_colors :: inline (foreground: Palette, background: Palette) { print( #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), cast(u8)foreground, cast(u8)background); } } else { #load "palette_24b.jai"; Color_24b :: struct { r: u8; g: u8; b: u8; } set_colors :: inline (foreground: Color_24b, background: Color_24b) { print( #run sprint(Commands.SetGraphicsRendition, "38;2;%;%;%;48;2;%;%;%"), foreground.r, foreground.g, foreground.b, background.r, background.g, background.b); } } #add_context tui_style: Style; Style :: struct { #if COLOR_BIT_DEPTH == 4 || COLOR_BIT_DEPTH == 8 { background: Palette = .BLACK; foreground: Palette = .WHITE; } else { background: Color_24b; foreground: Color_24b; } bold: bool; underline: bool; strike_through: bool; negative: bool; } set_font_style :: inline (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); } set_style :: (style: Style) { set_font_style(style.bold, style.underline, style.strike_through, style.negative); set_colors(style.foreground, style.background); context.tui_style = style; } clear_style :: () { write_string(#run sprint(Commands.SetGraphicsRendition, "0")); } using_style :: (style: Style) #expand { __style := context.tui_style; set_style(style); `defer set_style(__style); } // 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 { beginsWithEscape := ((key & 0xFF) ^ #char "#") == 0; hasSomethingElse := (key & (~0xFF)) != 0; return beginsWithEscape && hasSomethingElse; } 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 Provide an advanced read_input_line function that allows some styling and to set a placeholder text. read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { /* 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 Escape key. Resize should discard the input returning an empty string and a Resize key. */ assert(count_limit >= 0, "Invalid value on count_limit parameter."); str := alloc_string(count_limit); str.count = 0; idx := 0; // placeholder: string = "", // { // copy_size := ifx placeholder.count > str.count then str.count else placeholder.count; // memcpy(str.data, placeholder.data, copy_size); // idx = copy_size; // } // TODO Some of these may be nice to have: // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line row, col := get_cursor_position(); write_strings(Commands.StartBlinking, Commands.BlinkingUnderlineShape); key := Keys.None; while true { auto_release_temp(); key = get_key(); if key == { case Keys.Resize; #through; case Keys.Escape; #through; case Keys.Enter; break; case Keys.Left; if idx > 0 then idx -= 1; case Keys.Right; if idx < str.count then idx += 1; case Keys.Home; idx = 0; case Keys.End; idx = str.count; case Keys.Delete; if idx == str.count continue; for idx..str.count-2 { str.data[it] = str.data[it+1]; } str.data[str.count-1] = 0; str.count -= 1; case Keys.Backspace; if idx == 0 continue; idx -= 1; for idx..str.count-2 { str.data[it] = str.data[it+1]; } str.data[str.count-1] = 0; str.count -= 1; case; if idx >= count_limit continue; if is_escape_code(key) continue; for < count_limit..idx+1 { str.data[it] = str.data[it-1]; } key_str := to_string(key); str.data[idx] = key_str.data[0]; if str.count < count_limit then str.count += 1; idx += 1; } if is_visible { set_cursor_position(row, col); write_string(str); for str.count..count_limit-1 print_character(#char " "); } else { set_cursor_position(row, col); for 0..str.count-1 print_character(#char "*"); for str.count..count_limit-1 print_character(#char " "); } set_cursor_position(row, col+idx); } write_strings(Commands.StopBlinking, Commands.DefaultShape); 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! }