// 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"; #import "String"; #import "Thread"; // 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; } // 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; 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"; 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"); MetaUp : Key : #run to_key("\e[1;1A"); MetaDown : Key : #run to_key("\e[1;1B"); MetaRight : Key : #run to_key("\e[1;1C"); MetaLeft : Key : #run to_key("\e[1;1D"); ShiftUp : Key : #run to_key("\e[1;2A"); ShiftDown : Key : #run to_key("\e[1;2B"); ShiftRight : Key : #run to_key("\e[1;2C"); ShiftLeft : Key : #run to_key("\e[1;2D"); AltUp : Key : #run to_key("\e[1;3A"); AltDown : Key : #run to_key("\e[1;3B"); AltRight : Key : #run to_key("\e[1;3C"); AltLeft : Key : #run to_key("\e[1;3D"); AltShiftUp : Key : #run to_key("\e[1;4A"); AltShiftDown : Key : #run to_key("\e[1;4B"); AltShiftRight : Key : #run to_key("\e[1;4C"); AltShiftLeft : Key : #run to_key("\e[1;4D"); CtrlUp : Key : #run to_key("\e[1;5A"); CtrlDown : Key : #run to_key("\e[1;5B"); CtrlRight : Key : #run to_key("\e[1;5C"); CtrlLeft : Key : #run to_key("\e[1;5D"); CtrlShiftUp : Key : #run to_key("\e[1;6A"); CtrlShiftDown : Key : #run to_key("\e[1;6B"); CtrlShiftRight : Key : #run to_key("\e[1;6C"); CtrlShiftLeft : Key : #run to_key("\e[1;6D"); CtrlAltUp : Key : #run to_key("\e[1;7A"); CtrlAltDown : Key : #run to_key("\e[1;7B"); CtrlAltRight : Key : #run to_key("\e[1;7C"); CtrlAltLeft : Key : #run to_key("\e[1;7D"); CtrlAltShiftUp : Key : #run to_key("\e[1;7A"); CtrlAltShiftDown : Key : #run to_key("\e[1;7B"); CtrlAltShiftRight : Key : #run to_key("\e[1;7C"); CtrlAltShiftLeft : Key : #run to_key("\e[1;7D"); Home : Key : #run to_key("\e[H"); End : Key : #run to_key("\e[F"); Escape : Key : 0x00000000_0000001B; Backspace : Key : 0x00000000_0000007F; Pause : Key : 0x00000000_0000001A; Insert : Key : #run to_key("\e[2~"); Delete : Key : #run to_key("\e[3~"); PgUp : Key : #run to_key("\e[5~"); PgDown : Key : #run to_key("\e[6~"); /* TODO On get_key, convert F1 to F4 into the format "\e[1X~" so that: F1 : 1b 4f 50 : ^OP : -> \e[11~ Shift+ F1 : 1b 4f 32 50 : ^O2P : -> \e[11;2~ F2 : 1b 4f 51 : ^OQ : -> \e[12~ Shift+ F2 : 1b 4f 32 51 : ^O2Q : -> \e[12;2~ F3 : 1b 4f 52 : ^OR : -> \e[13~ Shift+ F3 : 1b 4f 32 52 : ^O2R : -> \e[13;2~ F4 : 1b 4f 53 : ^OS : -> \e[14~ Meta+ Fx : 1b 4f 31 53 : ^O1S : -> \e[14;1~ Shift+ F4 : 1b 4f 32 53 : ^O2S : -> \e[14;2~ Alt+ F4 : 1b 4f 33 53 : ^O3S : -> \e[14;3~ S+A F4 : 1b 4f 34 53 : ^O4S : -> \e[14;4~ Ctrl+ F4 : 1b 4f 35 53 : ^O4S : -> \e[14;5~ Ctrl+ F4 : 1b 4f 35 53 : ^O4S : -> \e[14;5~ ... */ // WIP HERE F1 : Key : #run to_key("\eOP"); F2 : Key : #run to_key("\eOQ"); F3 : Key : #run to_key("\eOR"); F4 : Key : #run to_key("\eOS"); F5 : Key : #run to_key("\e[15~"); F6 : Key : #run to_key("\e[17~"); F7 : Key : #run to_key("\e[18~"); F8 : Key : #run to_key("\e[19~"); F9 : Key : #run to_key("\e[20~"); F10 : Key : #run to_key("\e[21~"); F11 : Key : #run to_key("\e[23~"); F12 : Key : #run to_key("\e[24~"); MetaF1 : Key : #run to_key("\e[1;1P"); MetaF2 : Key : #run to_key("\e[1;1Q"); MetaF3 : Key : #run to_key("\e[1;1R"); MetaF4 : Key : #run to_key("\e[1;1S"); MetaF5 : Key : #run to_key("\e[15;1~"); MetaF6 : Key : #run to_key("\e[17;1~"); MetaF7 : Key : #run to_key("\e[18;1~"); MetaF8 : Key : #run to_key("\e[19;1~"); MetaF9 : Key : #run to_key("\e[20;1~"); MetaF10 : Key : #run to_key("\e[21;1~"); MetaF11 : Key : #run to_key("\e[23;1~"); MetaF12 : Key : #run to_key("\e[24;1~"); ShiftF1 : Key : #run to_key("\e[1;2P"); ShiftF2 : Key : #run to_key("\e[1;2Q"); ShiftF3 : Key : #run to_key("\e[1;2R"); ShiftF4 : Key : #run to_key("\e[1;2S"); ShiftF5 : Key : #run to_key("\e[15;2~"); ShiftF6 : Key : #run to_key("\e[17;2~"); ShiftF7 : Key : #run to_key("\e[18;2~"); ShiftF8 : Key : #run to_key("\e[19;2~"); ShiftF9 : Key : #run to_key("\e[20;2~"); ShiftF10 : Key : #run to_key("\e[21;2~"); ShiftF11 : Key : #run to_key("\e[23;2~"); ShiftF12 : Key : #run to_key("\e[24;2~"); AltF1 : Key : #run to_key("\e[1;3P"); AltF2 : Key : #run to_key("\e[1;3Q"); AltF3 : Key : #run to_key("\e[1;3R"); AltF4 : Key : #run to_key("\e[1;3S"); AltF5 : Key : #run to_key("\e[15;3~"); AltF6 : Key : #run to_key("\e[17;3~"); AltF7 : Key : #run to_key("\e[18;3~"); AltF8 : Key : #run to_key("\e[19;3~"); AltF9 : Key : #run to_key("\e[20;3~"); AltF10 : Key : #run to_key("\e[21;3~"); AltF11 : Key : #run to_key("\e[23;3~"); AltF12 : Key : #run to_key("\e[24;3~"); AltShiftF1 : Key : #run to_key("\e[1;4P"); AltShiftF2 : Key : #run to_key("\e[1;4Q"); AltShiftF3 : Key : #run to_key("\e[1;4R"); AltShiftF4 : Key : #run to_key("\e[1;4S"); AltShiftF5 : Key : #run to_key("\e[15;4~"); AltShiftF6 : Key : #run to_key("\e[17;4~"); AltShiftF7 : Key : #run to_key("\e[18;4~"); AltShiftF8 : Key : #run to_key("\e[19;4~"); AltShiftF9 : Key : #run to_key("\e[20;4~"); AltShiftF10 : Key : #run to_key("\e[21;4~"); AltShiftF11 : Key : #run to_key("\e[23;4~"); AltShiftF12 : Key : #run to_key("\e[24;4~"); CtrlF1 : Key : #run to_key("\e[1;5P"); CtrlF2 : Key : #run to_key("\e[1;5Q"); CtrlF3 : Key : #run to_key("\e[1;5R"); CtrlF4 : Key : #run to_key("\e[1;5S"); CtrlF5 : Key : #run to_key("\e[15;5~"); CtrlF6 : Key : #run to_key("\e[17;5~"); CtrlF7 : Key : #run to_key("\e[18;5~"); CtrlF8 : Key : #run to_key("\e[19;5~"); CtrlF9 : Key : #run to_key("\e[20;5~"); CtrlF10 : Key : #run to_key("\e[21;5~"); CtrlF11 : Key : #run to_key("\e[23;5~"); CtrlF12 : Key : #run to_key("\e[24;5~"); CtrlShiftF1 : Key : #run to_key("\e[1;6P"); CtrlShiftF2 : Key : #run to_key("\e[1;6Q"); CtrlShiftF3 : Key : #run to_key("\e[1;6R"); CtrlShiftF4 : Key : #run to_key("\e[1;6S"); CtrlShiftF5 : Key : #run to_key("\e[15;6~"); CtrlShiftF6 : Key : #run to_key("\e[17;6~"); CtrlShiftF7 : Key : #run to_key("\e[18;6~"); CtrlShiftF8 : Key : #run to_key("\e[19;6~"); CtrlShiftF9 : Key : #run to_key("\e[20;6~"); CtrlShiftF10 : Key : #run to_key("\e[21;6~"); CtrlShiftF11 : Key : #run to_key("\e[23;6~"); CtrlShiftF12 : Key : #run to_key("\e[24;6~"); CtrlAltF1 : Key : #run to_key("\e[1;7P"); CtrlAltF2 : Key : #run to_key("\e[1;7Q"); CtrlAltF3 : Key : #run to_key("\e[1;7R"); CtrlAltF4 : Key : #run to_key("\e[1;7S"); CtrlAltF5 : Key : #run to_key("\e[15;7~"); CtrlAltF6 : Key : #run to_key("\e[17;7~"); CtrlAltF7 : Key : #run to_key("\e[18;7~"); CtrlAltF8 : Key : #run to_key("\e[19;7~"); CtrlAltF9 : Key : #run to_key("\e[20;7~"); CtrlAltF10 : Key : #run to_key("\e[21;7~"); CtrlAltF11 : Key : #run to_key("\e[23;7~"); CtrlAltF12 : Key : #run to_key("\e[24;7~"); CtrlAltShiftF1 : Key : #run to_key("\e[1;8P"); CtrlAltShiftF2 : Key : #run to_key("\e[1;8Q"); CtrlAltShiftF3 : Key : #run to_key("\e[1;8R"); CtrlAltShiftF4 : Key : #run to_key("\e[1;8S"); CtrlAltShiftF5 : Key : #run to_key("\e[15;8~"); CtrlAltShiftF6 : Key : #run to_key("\e[17;8~"); CtrlAltShiftF7 : Key : #run to_key("\e[18;8~"); CtrlAltShiftF8 : Key : #run to_key("\e[19;8~"); CtrlAltShiftF9 : Key : #run to_key("\e[20;8~"); CtrlAltShiftF10 : Key : #run to_key("\e[21;8~"); CtrlAltShiftF11 : Key : #run to_key("\e[23;8~"); CtrlAltShiftF12 : Key : #run to_key("\e[24;8~"); } 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 { result := false; while key != 0 { key >>= 8; result |= ((key ^ Keys.Escape) == 0); } return result; } 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(); /* 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 { 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 { 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, to_parse.count); return key; } // TODO try_parse_escape_code // { // assert(false, "TODO try_parse_escape_code"); // } return xx Keys.None; } // 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; 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! }