#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 { 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 { EnterAlternateBuffer :: "\e[?1049h"; EnterMainBuffer :: "\e[?1049l"; EnterDrawingMode :: "\e(0"; EnterNormalMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; RefreshWindow :: "\e[7t"; // TODO Not yet tested. SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE // 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. } // TODO Maybe make the OS_* procedures as inline?! initialized := false; input_buffer_mutex: Mutex; input_buffer: string; input_counter: s64; read_buffer: [4096] u8; input_process_thread: Thread; Key :: u8; // TODO To be improved. Keys :: enum u8 { None :: 0; Resize :: 1; //410; // TODO Why?! } key_semaphore: Semaphore; key_mutex: Mutex; key_input := Keys.None; key_resize := Keys.None; // Notes // So, the semaphore usage should indicate the items on the key_queue. // If we don't want to use a queue, maybe we can use two Key items, one for user input, and another, with higher priority, for Resize. dam :: (msg: string) { print(msg, to_standard_error = true); } process_input :: (thread: *Thread) -> s64 { char: u8; dam(">START signal_input\n"); defer dam(">STOP signal_input\n"); while(true) { if initialized == false return 0; if key_input != Keys.None { dam("waiting.."); sleep_milliseconds(100); // We could increase this value as a push-back mechanism if we're just hitting it repeateadly. dam("!\n\r"); continue; } dam("reading.."); bytes_read := OS_read_input(*char, 1); dam("!\n\r"); if bytes_read > 0 { lock(*key_mutex); defer unlock(*key_mutex); key_input = xx char; dam("signaling.."); dam("!\n\r"); signal(*key_semaphore); } } return 0; } process_resize :: (signal : s32) #c_call { new_context : Context; push_context new_context { if signal != SIGWINCH return; lock(*key_mutex); defer unlock(*key_mutex); // TODO Only signal the key_semaphore if we don't already have a Resize on the queue. if key_resize != Key.None continue; key_resize = Keys.Resize; signal(*key_semaphore); } } get_key :: (wait_milliseconds: s32) -> Key { wait_for(*key_semaphore, wait_milliseconds); lock(*key_mutex); defer unlock(*key_mutex); if key_resize != Keys.None { defer key_resize = Keys.None; return xx key_resize; } defer key_input = Keys.None; return xx key_input; } start :: () { if initialized == true return; dam("A"); // write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); OS_prepare_terminal(); dam("B"); input_buffer = alloc_string(4096); init(*input_buffer_mutex, "input_buffer_mutex"); assert(thread_init(*input_process_thread, process_input), "Failed to initialize thread."); dam("C"); init(*key_semaphore); init(*key_mutex, "key_mutex"); dam("D"); initialized = true; thread_start(*input_process_thread); dam("E"); } stop :: () { if initialized == false return; initialized = false; thread_deinit(*input_process_thread); destroy(*key_semaphore); OS_reset_terminal(); // write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } get_str :: () -> string { lock(*input_buffer_mutex); defer unlock(*input_buffer_mutex); return input_buffer; // return tprint("%", to_string(input_buffer)); // print("%", tprint("%", input_buffer)); // return tprint("%", input_buffer); } flush_input :: () { // TODO lock(*input_buffer_mutex); defer unlock(*input_buffer_mutex); input_buffer.count = 0; } draw_box :: (x: int, y: int, width: int, height: int) { // 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.EnterDrawingMode, 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.EnterNormalMode); } // TODO Maybe rename to "clear()" clear_terminal :: inline () { assert(initialized, "TUI is not ready."); write_string(Commands.ClearScreen); } // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { assert(initialized, "TUI is not ready."); rows, columns := OS_get_terminal_size(); return rows, columns; } set_cursor_position :: (row: int, column: int) { auto_release_temp(); tmp_string := tprint(Commands.SetCursorPosition, row, column); write_string(tmp_string); } get_cursor_position :: () -> row: int, column: int { assert(initialized, "TUI is not ready."); // TODO Should I use this inside each and every procedure? auto_release_temp(); write_string(Commands.QueryCursorPosition); input := talloc_string(64); input.count = OS_read_input(input.data, input.count); // TODO Does not check for read errors. // Expected message format: \e[;R // where is the number of rows and of columns. assert( input[0] == #char "\e" && input[1] == #char "[" && input[input.count-1] == #char "R", "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; } Input_Mode :: enum u8 { HUMAN; // Shows cursor, echoes input, and expects an enter at the end of the line. MACHINE; // Hides cursor, hides input, and reads right away once the first input is available. } // read_input :: (allocator: Allocator = temp, $mode: Input_Mode = .HUMAN) -> string { // #if mode == .HUMAN { // write_string(Commands.ShowCursor); // defer write_string(Commands.HideCursor); // // OS_set_input_mode(.HUMAN); // defer OS_set_input_mode(.MACHINE); // } // // assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); // // #assert(mode != .MACHINE); // TODO Keep an eye if I try to use read_input for machine read. Eventually, remove mode from the procedure arguments. // // 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 == 0 || buffer_data[buffer.count-1] == #char "\n" break; // assert(buffer.count == buffer.allocated); // TODO If newline wasn't detected, it's because the buffer got full. // expand(*builder); // } // return builder_to_string(*builder, allocator); // } #if OS == .WINDOWS { // Prototyping zone... keep clear! } else #if OS == .LINUX || OS == .MACOS { // Prototyping zone... keep clear! }