#if OS == .WINDOWS { #load "windows.jai"; } else #if (OS == .LINUX) || (OS == .MACOS) { #load "unix.jai"; } else { #assert(false, "Unsupported OS."); } #import "Basic"; #import "String"; // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#designate-character-set // https://github.com/MicrosoftDocs/Console-Docs/blob/main/docs/console-virtual-terminal-sequences.md isTUIActive := false; // TODO Rename this variable. 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 rename to "setup()" start :: () { OS_prepare_terminal(); write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); isTUIActive = true; } // TODO Maybe rename to "reset()" stop :: () { isTUIActive = false; write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); OS_reset_terminal(); } 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 () { write_string(Commands.ClearScreen); } // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { rows, columns := OS_get_terminal_size(); return rows, columns; } // read_input: () -> string { // return OS_read_input(); // } 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(isTUIActive, "TUI is not active."); // TODO write_string(Commands.QueryCursorPosition); // Returned input := read_input(false); // Result: \e[21;1R assert( input.data[0] == #char "\e" && input.data[1] == #char "[" && input.data[input.count-1] == #char "R", "Query cursor position returned invalid response."); input.data += 2; print(">%<\n", input); parts := split(input, ";"); row := parse_int(*parts[0]); column := parse_int(*parts[1]); return row, column; } read_input :: (blocking: bool = true) -> string { return OS_read_input(blocking); } // read_input: () -> string; #if OS == .WINDOWS { OS_read_input :: (blocking: bool) -> string { result: string = ---; MAX_BYTES_TO_READ :: 1024; temp : [MAX_BYTES_TO_READ] u8; bytes_read : s32; if ReadConsoleA(stdin, temp.data, xx temp.count, *bytes_read) { result.data = alloc(bytes_read); result.count = bytes_read; memcpy(result.data, temp.data, bytes_read); } return result; }; } else #if OS == .LINUX || OS == .MACOS { OS_read_input :: (blocking: bool) -> string { term : My_Termios; tcgetattr(STDIN_FILENO, *term); // Input modes. Input_Modes :: enum u32 { IGNBRK; // Ignore break condition. BRKINT; // Signal interrupt on break. IGNPAR; // Ignore characters with parity errors. PARMRK; // Mark parity and framing errors. INPCK; // Enable input parity check. ISTRIP; // Strip 8th bit off characters. INLCR; // Map NL to CR on input. IGNCR; // Ignore CR. ICRNL; // Map CR to NL on input. IXON; // Enable start/stop output control. IXOFF; // Enable start/stop input control. IXANY; // Any character will restart after stop. __NOT_USED__; IMAXBEL; // Ring bell when input queue is full. IUCLC; // Translate upper case input to lower case. } // IGNBRK :u32: (1 << 0); // Ignore break condition. // BRKINT :u32: (1 << 1); // Signal interrupt on break. // IGNPAR :u32: (1 << 2); // Ignore characters with parity errors. // PARMRK :u32: (1 << 3); // Mark parity and framing errors. // INPCK :u32: (1 << 4); // Enable input parity check. // ISTRIP :u32: (1 << 5); // Strip 8th bit off characters. // INLCR :u32: (1 << 6); // Map NL to CR on input. // IGNCR :u32: (1 << 7); // Ignore CR. // ICRNL :u32: (1 << 8); // Map CR to NL on input. // IXON :u32: (1 << 9); // Enable start/stop output control. // IXOFF :u32: (1 << 10); // Enable start/stop input control. // IXANY :u32: (1 << 11); // Any character will restart after stop. // IMAXBEL :u32: (1 << 13); // Ring bell when input queue is full. // IUCLC :u32: (1 << 14); // Translate upper case input to lower case. // IGNBRK :u32: 0x0001; // BRKINT :u32: 0x0002; // IGNCR :u32: 0x0040; ECHO :u32: 0x0004; ECHONL :u32: 0x0010; ICANON :u32: 0x0080; IEXTEN :u32: 0x0400; backup: My_Termios; tcgetattr(STDIN_FILENO, *backup); if blocking { term = __term; // iflags: Input_Modes = (.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); // term.c_iflag |= xx iflags; // term.c_lflag |= (ECHO | ECHONL | ICANON | IEXTEN); } else { iflags: Input_Modes = (.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); term.c_iflag &= xx ~(iflags); term.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN); } // term.c_iflag &= 0xFFFFFA14;// ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); // term.c_oflag &= 0xFFFFFFFE;// ~OPOST; // term.c_lflag &= 0xFFFF7FB4;// ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); // term.c_cflag &= 0xFFFFFECF;// ~(CSIZE | PARENB); // term.c_cflag |= 0x00000030; tcsetattr(STDIN_FILENO, 0, *term); result: string = ---; buffer: [8192] u8; write_string(Commands.ShowCursor); bytes_read := read(STDIN_FILENO, buffer.data, buffer.count-1); write_string(Commands.HideCursor); // TODO WIP WIP WIP result = to_string(buffer.data, bytes_read); // TODO WIP WIP WIP This is still using the stack allocated buffer and WILL FAIL! tcsetattr(STDIN_FILENO, 0, *backup); return result; }; }