From 7d358263c8b7dd154f2e7f049659f4c706ab5b75 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 26 May 2024 01:22:59 +0100 Subject: Saturation, TUI, and UTF8 version 1.0. --- TUI/examples/snake.jai | 188 ++++++++++++ TUI/key_map.jai | 510 ++++++++++++++++++++++++++++++ TUI/module.jai | 817 +++++++++++++++++++++++++++++++++++++++++++++++++ TUI/palette_24b.jai | 50 +++ TUI/palette_4b.jai | 19 ++ TUI/palette_8b.jai | 307 +++++++++++++++++++ TUI/tests.jai | 232 ++++++++++++++ TUI/unix.jai | 319 +++++++++++++++++++ TUI/windows.jai | 390 +++++++++++++++++++++++ 9 files changed, 2832 insertions(+) create mode 100644 TUI/examples/snake.jai create mode 100644 TUI/key_map.jai create mode 100644 TUI/module.jai create mode 100644 TUI/palette_24b.jai create mode 100644 TUI/palette_4b.jai create mode 100644 TUI/palette_8b.jai create mode 100644 TUI/tests.jai create mode 100644 TUI/unix.jai create mode 100644 TUI/windows.jai (limited to 'TUI') diff --git a/TUI/examples/snake.jai b/TUI/examples/snake.jai new file mode 100644 index 0000000..b62136c --- /dev/null +++ b/TUI/examples/snake.jai @@ -0,0 +1,188 @@ +#import "Basic"; +#import "Math"; +#import "Random"; +TUI :: #import "TUI"(COLOR_MODE_BITS = 4); + +screen_size_x: int = ---; +screen_size_y: int = ---; +player_name: string = ---; + +main :: () { + // Randomize initial random state. + seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. + random_seed(seed); + + assert(TUI.setup_terminal(), "Failed to setup TUI."); + + // Ask for the player name, and keep it limited to 64 bytes. + TUI.set_cursor_position(1, 1); + write_string("Please enter player name: "); + player_name = TUI.read_input_line(64); + + while true { + + game_loop(); + + // Draw the game over screen. + BOX_SIZE_X :: 20; + BOX_SIZE_Y :: 4; + GAME_OVER_TEXT :: "~ game over ~"; #assert(GAME_OVER_TEXT.count < BOX_SIZE_X-2); + INSTRUCTIONS_TEXT :: "(esc to exit)"; #assert(INSTRUCTIONS_TEXT.count < BOX_SIZE_X-2); + + TUI.draw_box((screen_size_x-BOX_SIZE_X)/2, (screen_size_y-BOX_SIZE_Y)/2, BOX_SIZE_X, BOX_SIZE_Y, true); + TUI.set_cursor_position((screen_size_x-GAME_OVER_TEXT.count)/2, (screen_size_y-BOX_SIZE_Y)/2 + 1); + write_string(GAME_OVER_TEXT); + TUI.set_cursor_position((screen_size_x-INSTRUCTIONS_TEXT.count)/2, (screen_size_y-BOX_SIZE_Y)/2 + 2); + write_string(INSTRUCTIONS_TEXT); + sleep_milliseconds(100); // Avoid any sudden player input. + + // Wait for user input, and exit if the user presses Escape. + TUI.flush_input(); + if TUI.get_key() == TUI.Keys.Escape then break; + } + + assert(TUI.reset_terminal(), "Failed to reset TUI."); +} + +game_loop :: () { + + Vec2D :: struct { + x: int; + y: int; + } + + operator == :: (a: Vec2D, b: Vec2D) -> bool { + return a.x == b.x && a.y == b.y; + } + + LOOP_PERIOD_MS :: 66; + + // Setup game state. + score := 0; + dir := Vec2D.{1, 0}; + food := Vec2D.{5, 5}; + snake_parts: [..] Vec2D; + for 0..13 array_add(*snake_parts, Vec2D.{3, 3}); + snake_parts[0].x += 1; + + // Use the default foreground and background colors. + TUI.set_style(.{ use_default_background_color = true, use_default_foreground_color = true }); + + // Force to draw the game UI by simulating a terminal resize. + TUI.flush_input(); + TUI.set_next_key(TUI.Keys.Resize); + + while main_loop := true { + + // Setup the module's context string builder to buffer the output on temporary memory and print everything at once. + auto_release_temp(); + temp_builder := String_Builder.{ allocator = temporary_allocator }; + TUI.using_builder_as_output(*temp_builder); + defer write_builder(*temp_builder); + + // Redirect text output to TUI functions to make use of module's context string builder. + print :: TUI.tui_print; + write_string :: TUI.tui_write_string; + + timestamp := current_time_monotonic(); + key := TUI.get_key(LOOP_PERIOD_MS); + + if key == { + case TUI.Keys.Resize; + // Draw game UI. + TUI.clear_terminal(); + screen_size_x, screen_size_y = TUI.get_terminal_size(); + TUI.draw_box(1, 1, screen_size_x, screen_size_y); + TUI.set_cursor_position(3, screen_size_y); + print(" % ", player_name); + + case TUI.Keys.Escape; + break main_loop; + + case TUI.Keys.Up; + if dir != .{0, 1} then dir = .{0, -1}; + + case TUI.Keys.Down; + if dir != .{0, -1} then dir = .{0, 1}; + + case TUI.Keys.Left; + if dir != .{1, 0} then dir = .{-1, 0}; + + case TUI.Keys.Right; + if dir != .{-1, 0} then dir = .{1, 0}; + } + + // Pause game if screen is too small. + if screen_size_x < 15 || screen_size_y < 15 { + TUI.clear_terminal(); + TUI.set_cursor_position(1,1); + write_string("~ paused : increase window size ~"); + continue; + } + + // Keep snake's last position so we can clear it from screen. + last_pos := snake_parts[snake_parts.count-1]; + + // Update snake position. + for < snake_parts.count-1..1 { + if snake_parts[it] != snake_parts[it-1] { + snake_parts[it] = snake_parts[it-1]; + } + } + snake_parts[0].x += dir.x; + snake_parts[0].y += dir.y; + + // Teleport on borders. + if snake_parts[0].x < 2 then snake_parts[0].x = screen_size_x - 1; + if snake_parts[0].x >= screen_size_x then snake_parts[0].x = 2; + if snake_parts[0].y < 2 then snake_parts[0].y = screen_size_y - 1; + if snake_parts[0].y >= screen_size_y then snake_parts[0].y = 2; + food.x = clamp(food.x, 2, screen_size_x-1); + food.y = clamp(food.y, 2, screen_size_y-1); + + // Check for game-over. + for 1..snake_parts.count-1 { + if snake_parts[it] == snake_parts[0] { + break main_loop; + } + } + + // Check for food. + if snake_parts[0] == food { + score += 1; + array_add(*snake_parts, snake_parts[snake_parts.count-1]); + food = Vec2D.{ + cast(int)(random_get_zero_to_one_open() * (screen_size_x-3) + 2), + cast(int)(random_get_zero_to_one_open() * (screen_size_y-3) + 2) + }; + } + + // Wait to match game loop time. + delta := to_milliseconds(current_time_monotonic() - timestamp); + if delta < LOOP_PERIOD_MS { + sleep_milliseconds(xx (LOOP_PERIOD_MS - delta)); + } + + // Draw snake. + { + write_string(TUI.Commands.DrawingMode); + TUI.set_cursor_position(last_pos.x, last_pos.y); + write_string(TUI.Drawings.Blank); + for snake_parts { + TUI.set_cursor_position(it.x, it.y); + write_string(TUI.Drawings.Checkerboard); + } + } + // Draw food. + { + TUI.using_style(TUI.Style.{ foreground = TUI.Palette.RED, bold = true, use_default_background_color = true }); + TUI.set_cursor_position(food.x, food.y); + write_string(TUI.Drawings.Diamond); + } + write_string(TUI.Commands.TextMode); + + // Draw score. + TUI.set_cursor_position(3, 1); + print(" % ", score); + } +} diff --git a/TUI/key_map.jai b/TUI/key_map.jai new file mode 100644 index 0000000..43762f4 --- /dev/null +++ b/TUI/key_map.jai @@ -0,0 +1,510 @@ +#import "Hash_Table"; + +key_map: Table(string, Key); + +reset_key_map :: () { + table_reset(*key_map); + deinit(*key_map); +} + +setup_key_map :: () { + + if key_map.count > 0 then return; + + /* + This table was created/tested using the following terminals: + - g: gnome terminal + - i: kitty + - k: konsole + - l: linux console + - w: windows terminal + - x: xterm + + To signal modifier keys, a letter is appended after a + (plus sign): + "#f1" -> F1 + "#f1+$" -> F1 + Shift + "#f1+a" -> F1 + Alt + "#f1+A" -> F1 + Shift + Alt + "#f1+c" -> F1 + Ctrl + "#f1+C" -> F1 + Shift + Ctrl + "#f1+w" -> F1 + Alt + Ctrl + "#f1+W" -> F1 + Shift + Alt + Ctrl + "#f1+s" -> F1 + Super + "#f1+S" -> F1 + Shift + Super + "#f1+x" -> F1 + Alt + Super + "#f1+X" -> F1 + Shift + Alt + Super + "#f1+y" -> F1 + Ctrl + Super + "#f1+Y" -> F1 + Shift + Ctrl + Super + "#f1+z" -> F1 + Alt + Ctrl + Super + "#f1+Z" -> F1 + Shift + Alt + Ctrl + Super + */ + + // Up // g i k l w x + table_set(*key_map, "\e[A", to_key("#up")); // + + + + + + + table_set(*key_map, "\e[1;1A", to_key("#up")); // + table_set(*key_map, "\e[1;2A", to_key("#up+$")); // + + + + + + table_set(*key_map, "\e[1;3A", to_key("#up+a")); // + + + + + + table_set(*key_map, "\e[1;4A", to_key("#up+A")); // + + + + + table_set(*key_map, "\e[1;5A", to_key("#up+c")); // + + + + + + table_set(*key_map, "\e[1;6A", to_key("#up+C")); // + + + + + table_set(*key_map, "\e[1;7A", to_key("#up+w")); // + + + + + + table_set(*key_map, "\e[1;8A", to_key("#up+W")); // + + + + + + table_set(*key_map, "\e[1;9A", to_key("#up+s")); // + + table_set(*key_map, "\e[1;10A", to_key("#up+S")); // + + table_set(*key_map, "\e[1;11A", to_key("#up+x")); // + + table_set(*key_map, "\e[1;12A", to_key("#up+X")); // + + table_set(*key_map, "\e[1;13A", to_key("#up+y")); // + + table_set(*key_map, "\e[1;14A", to_key("#up+Y")); // + + table_set(*key_map, "\e[1;15A", to_key("#up+z")); // + + table_set(*key_map, "\e[1;16A", to_key("#up+Z")); // + + + // Down // g i k l w x + table_set(*key_map, "\e[B", to_key("#down")); // + + + + + + + table_set(*key_map, "\e[1;1B", to_key("#down")); // + table_set(*key_map, "\e[1;2B", to_key("#down+$")); // + + + + + + table_set(*key_map, "\e[1;3B", to_key("#down+a")); // + + + + + + table_set(*key_map, "\e[1;4B", to_key("#down+A")); // + + + + + table_set(*key_map, "\e[1;5B", to_key("#down+c")); // + + + + + + table_set(*key_map, "\e[1;6B", to_key("#down+C")); // + + + + + table_set(*key_map, "\e[1;7B", to_key("#down+w")); // + + + + + + table_set(*key_map, "\e[1;8B", to_key("#down+W")); // + + + + + + table_set(*key_map, "\e[1;9B", to_key("#down+s")); // + + table_set(*key_map, "\e[1;10B", to_key("#down+S")); // + + table_set(*key_map, "\e[1;11B", to_key("#down+x")); // + + table_set(*key_map, "\e[1;12B", to_key("#down+X")); // + + table_set(*key_map, "\e[1;13B", to_key("#down+y")); // + + table_set(*key_map, "\e[1;14B", to_key("#down+Y")); // + + table_set(*key_map, "\e[1;15B", to_key("#down+z")); // + + table_set(*key_map, "\e[1;16B", to_key("#down+Z")); // + + + // Right // g i k l w x + table_set(*key_map, "\e[C", to_key("#right")); // + + + + + + + table_set(*key_map, "\e[1;1C", to_key("#right")); // + table_set(*key_map, "\e[1;2C", to_key("#right+$")); // + + + + + + table_set(*key_map, "\e[1;3C", to_key("#right+a")); // + + + + + + table_set(*key_map, "\e[1;4C", to_key("#right+A")); // + + + + + table_set(*key_map, "\e[1;5C", to_key("#right+c")); // + + + + + + table_set(*key_map, "\e[1;6C", to_key("#right+C")); // + + + + + + table_set(*key_map, "\e[1;7C", to_key("#right+w")); // + + + + + + table_set(*key_map, "\e[1;8C", to_key("#right+W")); // + + + + + + table_set(*key_map, "\e[1;9C", to_key("#right+s")); // + + table_set(*key_map, "\e[1;10C", to_key("#right+S")); // + + table_set(*key_map, "\e[1;11C", to_key("#right+x")); // + + table_set(*key_map, "\e[1;12C", to_key("#right+X")); // + + table_set(*key_map, "\e[1;13C", to_key("#right+y")); // + + table_set(*key_map, "\e[1;14C", to_key("#right+Y")); // + + table_set(*key_map, "\e[1;15C", to_key("#right+z")); // + + table_set(*key_map, "\e[1;16C", to_key("#right+Z")); // + + + // Left // g i k l w x + table_set(*key_map, "\e[D", to_key("#left")); // + + + + + + + table_set(*key_map, "\e[1;1D", to_key("#left")); // + table_set(*key_map, "\e[1;2D", to_key("#left+$")); // + + + + + + table_set(*key_map, "\e[1;3D", to_key("#left+a")); // + + + + + + table_set(*key_map, "\e[1;4D", to_key("#left+A")); // + + + + + + table_set(*key_map, "\e[1;5D", to_key("#left+c")); // + + + + + + table_set(*key_map, "\e[1;6D", to_key("#left+C")); // + + + + + + table_set(*key_map, "\e[1;7D", to_key("#left+w")); // + + + + + + table_set(*key_map, "\e[1;8D", to_key("#left+W")); // + + + + + + table_set(*key_map, "\e[1;9D", to_key("#left+s")); // + + table_set(*key_map, "\e[1;10D", to_key("#left+S")); // + + table_set(*key_map, "\e[1;11D", to_key("#left+x")); // + + table_set(*key_map, "\e[1;12D", to_key("#left+X")); // + + table_set(*key_map, "\e[1;13D", to_key("#left+y")); // + + table_set(*key_map, "\e[1;14D", to_key("#left+Y")); // + + table_set(*key_map, "\e[1;15D", to_key("#left+z")); // + + table_set(*key_map, "\e[1;16D", to_key("#left+Z")); // + + + // Home // g i k l w x + table_set(*key_map, "\e[1~", to_key("#home")); // + + table_set(*key_map, "\e[H", to_key("#home")); // + + + + + + table_set(*key_map, "\e[1;1H", to_key("#home")); // + table_set(*key_map, "\e[1;2H", to_key("#home+$")); // + + + + + + table_set(*key_map, "\e[1;3H", to_key("#home+a")); // + + + + + + table_set(*key_map, "\e[1;4H", to_key("#home+A")); // + + + + + + table_set(*key_map, "\e[1;5H", to_key("#home+c")); // + + + + + + table_set(*key_map, "\e[1;6H", to_key("#home+C")); // + + + + + table_set(*key_map, "\e[1;7H", to_key("#home+w")); // + + + + + + table_set(*key_map, "\e[1;8H", to_key("#home+W")); // + + + + + + table_set(*key_map, "\e[1;9H", to_key("#home+s")); // + + table_set(*key_map, "\e[1;10H", to_key("#home+S")); // + + table_set(*key_map, "\e[1;11H", to_key("#home+x")); // + + table_set(*key_map, "\e[1;12H", to_key("#home+X")); // + + table_set(*key_map, "\e[1;13H", to_key("#home+y")); // + + table_set(*key_map, "\e[1;14H", to_key("#home+Y")); // + + table_set(*key_map, "\e[1;15H", to_key("#home+z")); // + + table_set(*key_map, "\e[1;16H", to_key("#home+Z")); // + + + // End // g i k l w x + table_set(*key_map, "\e[4~", to_key("#end")); // + + table_set(*key_map, "\e[F", to_key("#end")); // + + + + + + table_set(*key_map, "\e[1;1F", to_key("#end")); // + table_set(*key_map, "\e[1;2F", to_key("#end+$")); // + + + + + + table_set(*key_map, "\e[1;3F", to_key("#end+a")); // + + + + + + table_set(*key_map, "\e[1;4F", to_key("#end+A")); // + + + + + + table_set(*key_map, "\e[1;5F", to_key("#end+c")); // + + + + + table_set(*key_map, "\e[1;6F", to_key("#end+C")); // + + + + + table_set(*key_map, "\e[1;7F", to_key("#end+w")); // + + + + + + table_set(*key_map, "\e[1;8F", to_key("#end+W")); // + + + + + + table_set(*key_map, "\e[1;9F", to_key("#end+s")); // + + table_set(*key_map, "\e[1;10F", to_key("#end+S")); // + + table_set(*key_map, "\e[1;11F", to_key("#end+x")); // + + table_set(*key_map, "\e[1;12F", to_key("#end+X")); // + + table_set(*key_map, "\e[1;13F", to_key("#end+y")); // + + table_set(*key_map, "\e[1;14F", to_key("#end+Y")); // + + table_set(*key_map, "\e[1;15F", to_key("#end+z")); // + + table_set(*key_map, "\e[1;16F", to_key("#end+Z")); // + + + // Insert // g i k l w x + table_set(*key_map, "\e[2~", to_key("#ins")); // + + + + + + + table_set(*key_map, "\e[2;1~", to_key("#ins")); // + table_set(*key_map, "\e[2;2~", to_key("#ins+$")); // + + + + + + table_set(*key_map, "\e[2;3~", to_key("#ins+a")); // + + + + + + table_set(*key_map, "\e[2;4~", to_key("#ins+A")); // + + + + + + table_set(*key_map, "\e[2;5~", to_key("#ins+c")); // + + + + + + table_set(*key_map, "\e[2;6~", to_key("#ins+C")); // + + + + + + table_set(*key_map, "\e[2;7~", to_key("#ins+w")); // + + + + + + table_set(*key_map, "\e[2;8~", to_key("#ins+W")); // + + + + + + table_set(*key_map, "\e[2;9~", to_key("#ins+s")); // + + table_set(*key_map, "\e[2;10~", to_key("#ins+S")); // + + table_set(*key_map, "\e[2;11~", to_key("#ins+x")); // + + table_set(*key_map, "\e[2;12~", to_key("#ins+X")); // + + table_set(*key_map, "\e[2;13~", to_key("#ins+y")); // + + table_set(*key_map, "\e[2;14~", to_key("#ins+Y")); // + + table_set(*key_map, "\e[2;15~", to_key("#ins+z")); // + + table_set(*key_map, "\e[2;16~", to_key("#ins+Z")); // + + + // Delete // g i k l w x + table_set(*key_map, "\e[3~", to_key("#del")); // + + + + + + + table_set(*key_map, "\e[3;1~", to_key("#del")); // + table_set(*key_map, "\e[3;2~", to_key("#del+$")); // + + + + + + table_set(*key_map, "\e[3;3~", to_key("#del+a")); // + + + + + + table_set(*key_map, "\e[3;4~", to_key("#del+A")); // + + + + + + table_set(*key_map, "\e[3;5~", to_key("#del+c")); // + + + + + + table_set(*key_map, "\e[3;6~", to_key("#del+C")); // + + + + + + table_set(*key_map, "\e[3;7~", to_key("#del+w")); // + + + + + + table_set(*key_map, "\e[3;8~", to_key("#del+W")); // + + + + + + table_set(*key_map, "\e[3;9~", to_key("#del+s")); // + + table_set(*key_map, "\e[3;10~", to_key("#del+S")); // + + table_set(*key_map, "\e[3;11~", to_key("#del+x")); // + + table_set(*key_map, "\e[3;12~", to_key("#del+X")); // + + table_set(*key_map, "\e[3;13~", to_key("#del+y")); // + + table_set(*key_map, "\e[3;14~", to_key("#del+Y")); // + + table_set(*key_map, "\e[3;15~", to_key("#del+z")); // + + table_set(*key_map, "\e[3;16~", to_key("#del+Z")); // + + + // Page Up // g i k l w x + table_set(*key_map, "\e[5~", to_key("#pup")); // + + + + + + + table_set(*key_map, "\e[5;1~", to_key("#pup")); // + table_set(*key_map, "\e[5;2~", to_key("#pup+$")); // + + + + + + table_set(*key_map, "\e[5;3~", to_key("#pup+a")); // + + + + + + table_set(*key_map, "\e[5;4~", to_key("#pup+A")); // + + + + + + table_set(*key_map, "\e[5;5~", to_key("#pup+c")); // + + + + + + table_set(*key_map, "\e[5;6~", to_key("#pup+C")); // + + + + + table_set(*key_map, "\e[5;7~", to_key("#pup+w")); // + + + + + + table_set(*key_map, "\e[5;8~", to_key("#pup+W")); // + + + + + + table_set(*key_map, "\e[5;9~", to_key("#pup+s")); // + + table_set(*key_map, "\e[5;10~", to_key("#pup+S")); // + + table_set(*key_map, "\e[5;11~", to_key("#pup+x")); // + + table_set(*key_map, "\e[5;12~", to_key("#pup+X")); // + + table_set(*key_map, "\e[5;13~", to_key("#pup+y")); // + + table_set(*key_map, "\e[5;14~", to_key("#pup+Y")); // + + table_set(*key_map, "\e[5;15~", to_key("#pup+z")); // + + table_set(*key_map, "\e[5;16~", to_key("#pup+Z")); // + + + // Page Down // g i k l w x + table_set(*key_map, "\e[6~", to_key("#pdown")); // + + + + + + + table_set(*key_map, "\e[6;1~", to_key("#pdown")); // + table_set(*key_map, "\e[6;2~", to_key("#pdown+$")); // + + + + + + table_set(*key_map, "\e[6;3~", to_key("#pdown+a")); // + + + + + + table_set(*key_map, "\e[6;4~", to_key("#pdown+A")); // + + + + + + table_set(*key_map, "\e[6;5~", to_key("#pdown+c")); // + + + + + + table_set(*key_map, "\e[6;6~", to_key("#pdown+C")); // + + + + + table_set(*key_map, "\e[6;7~", to_key("#pdown+w")); // + + + + + + table_set(*key_map, "\e[6;8~", to_key("#pdown+W")); // + + + + + + table_set(*key_map, "\e[6;9~", to_key("#pdown+s")); // + + table_set(*key_map, "\e[6;10~", to_key("#pdown+S")); // + + table_set(*key_map, "\e[6;11~", to_key("#pdown+x")); // + + table_set(*key_map, "\e[6;12~", to_key("#pdown+X")); // + + table_set(*key_map, "\e[6;13~", to_key("#pdown+y")); // + + table_set(*key_map, "\e[6;14~", to_key("#pdown+Y")); // + + table_set(*key_map, "\e[6;15~", to_key("#pdown+z")); // + + table_set(*key_map, "\e[6;16~", to_key("#pdown+Z")); // + + + // F1 // g i k l w x + table_set(*key_map, "\e[[A", to_key("#f1")); // + + table_set(*key_map, "\e[25~", to_key("#f1+$")); // + + table_set(*key_map, "\eOP", to_key("#f1")); // + + + + + + table_set(*key_map, "\eO1P", to_key("#f1+s")); // + + table_set(*key_map, "\eO2P", to_key("#f1+$")); // + + table_set(*key_map, "\eO3P", to_key("#f1+a")); // + + table_set(*key_map, "\eO4P", to_key("#f1+A")); // + + table_set(*key_map, "\eO5P", to_key("#f1+c")); // + + table_set(*key_map, "\eO6P", to_key("#f1+C")); // + + table_set(*key_map, "\eO7P", to_key("#f1+w")); // + + table_set(*key_map, "\eO8P", to_key("#f1+W")); // + + table_set(*key_map, "\e[1P", to_key("#f1")); // + table_set(*key_map, "\e[1;1P", to_key("#f1")); // + table_set(*key_map, "\e[1;2P", to_key("#f1+$")); // + + + + + table_set(*key_map, "\e[1;3P", to_key("#f1+a")); // + + + + + table_set(*key_map, "\e[1;4P", to_key("#f1+A")); // + + + + + table_set(*key_map, "\e[1;5P", to_key("#f1+c")); // + + + + + table_set(*key_map, "\e[1;6P", to_key("#f1+C")); // + + + + + table_set(*key_map, "\e[1;7P", to_key("#f1+w")); // + + + + + table_set(*key_map, "\e[1;8P", to_key("#f1+W")); // + + + + + table_set(*key_map, "\e[1;9P", to_key("#f1+s")); // + + table_set(*key_map, "\e[1;10P", to_key("#f1+S")); // + + table_set(*key_map, "\e[1;11P", to_key("#f1+x")); // + + table_set(*key_map, "\e[1;12P", to_key("#f1+X")); // + + table_set(*key_map, "\e[1;13P", to_key("#f1+y")); // + + table_set(*key_map, "\e[1;14P", to_key("#f1+Y")); // + + table_set(*key_map, "\e[1;15P", to_key("#f1+z")); // + + table_set(*key_map, "\e[1;16P", to_key("#f1+Z")); // + + + // F2 // g i k l w x + table_set(*key_map, "\e[[B", to_key("#f2")); // + + table_set(*key_map, "\e[26~", to_key("#f2+$")); // + + table_set(*key_map, "\eOQ", to_key("#f2")); // + + + + + + table_set(*key_map, "\eO1Q", to_key("#f2+s")); // + + table_set(*key_map, "\eO2Q", to_key("#f2+$")); // + + table_set(*key_map, "\eO3Q", to_key("#f2+a")); // + + table_set(*key_map, "\eO4Q", to_key("#f2+A")); // + + table_set(*key_map, "\eO5Q", to_key("#f2+c")); // + + table_set(*key_map, "\eO6Q", to_key("#f2+C")); // + + table_set(*key_map, "\eO7Q", to_key("#f2+w")); // + + table_set(*key_map, "\eO8Q", to_key("#f2+W")); // + + table_set(*key_map, "\e[1Q", to_key("#f2")); // + table_set(*key_map, "\e[1;1Q", to_key("#f2")); // + table_set(*key_map, "\e[1;2Q", to_key("#f2+$")); // + + + + + table_set(*key_map, "\e[1;3Q", to_key("#f2+a")); // + + + + + table_set(*key_map, "\e[1;4Q", to_key("#f2+A")); // + + + + + table_set(*key_map, "\e[1;5Q", to_key("#f2+c")); // + + + + + table_set(*key_map, "\e[1;6Q", to_key("#f2+C")); // + + + + + table_set(*key_map, "\e[1;7Q", to_key("#f2+w")); // + + + + + table_set(*key_map, "\e[1;8Q", to_key("#f2+W")); // + + + + + table_set(*key_map, "\e[1;9Q", to_key("#f2+s")); // + + table_set(*key_map, "\e[1;10Q", to_key("#f2+S")); // + + table_set(*key_map, "\e[1;11Q", to_key("#f2+x")); // + + table_set(*key_map, "\e[1;12Q", to_key("#f2+X")); // + + table_set(*key_map, "\e[1;13Q", to_key("#f2+y")); // + + table_set(*key_map, "\e[1;14Q", to_key("#f2+Y")); // + + table_set(*key_map, "\e[1;15Q", to_key("#f2+z")); // + + table_set(*key_map, "\e[1;16Q", to_key("#f2+Z")); // + + + // F3 // g i k l w x + table_set(*key_map, "\e[[C", to_key("#f3")); // + + table_set(*key_map, "\e[28~", to_key("#f3+$")); // + + table_set(*key_map, "\eOR", to_key("#f3")); // + + + + + + table_set(*key_map, "\eO1R", to_key("#f3+s")); // + + table_set(*key_map, "\eO2R", to_key("#f3+$")); // + + table_set(*key_map, "\eO3R", to_key("#f3+a")); // + + table_set(*key_map, "\eO4R", to_key("#f3+A")); // + + table_set(*key_map, "\eO5R", to_key("#f3+c")); // + + table_set(*key_map, "\eO6R", to_key("#f3+C")); // + + table_set(*key_map, "\eO7R", to_key("#f3+w")); // + + table_set(*key_map, "\eO8R", to_key("#f3+W")); // + + table_set(*key_map, "\e[1R", to_key("#f3")); // + table_set(*key_map, "\e[1;1R", to_key("#f3")); // + table_set(*key_map, "\e[1;2R", to_key("#f3+$")); // + + + + + table_set(*key_map, "\e[1;3R", to_key("#f3+a")); // + + + + + table_set(*key_map, "\e[1;4R", to_key("#f3+A")); // + + + + + table_set(*key_map, "\e[1;5R", to_key("#f3+c")); // + + + + + table_set(*key_map, "\e[1;6R", to_key("#f3+C")); // + + + + + table_set(*key_map, "\e[1;7R", to_key("#f3+w")); // + + + + + table_set(*key_map, "\e[1;8R", to_key("#f3+W")); // + + + + + table_set(*key_map, "\e[1;9R", to_key("#f3+s")); // + + table_set(*key_map, "\e[1;10R", to_key("#f3+S")); // + + table_set(*key_map, "\e[1;11R", to_key("#f3+x")); // + + table_set(*key_map, "\e[1;12R", to_key("#f3+X")); // + + table_set(*key_map, "\e[1;13R", to_key("#f3+y")); // + + table_set(*key_map, "\e[1;14R", to_key("#f3+Y")); // + + table_set(*key_map, "\e[1;15R", to_key("#f3+z")); // + + table_set(*key_map, "\e[1;16R", to_key("#f3+Z")); // + + + // F4 // g i k l w x + table_set(*key_map, "\e[[D", to_key("#f4")); // + + table_set(*key_map, "\e[29~", to_key("#f4+$")); // + + table_set(*key_map, "\eOS", to_key("#f4")); // + + + + + + table_set(*key_map, "\eO1S", to_key("#f4+s")); // + + table_set(*key_map, "\eO2S", to_key("#f4+$")); // + + table_set(*key_map, "\eO3S", to_key("#f4+a")); // + + table_set(*key_map, "\eO4S", to_key("#f4+A")); // + + table_set(*key_map, "\eO5S", to_key("#f4+c")); // + + table_set(*key_map, "\eO6S", to_key("#f4+C")); // + + table_set(*key_map, "\eO7S", to_key("#f4+w")); // + + table_set(*key_map, "\eO8S", to_key("#f4+W")); // + + table_set(*key_map, "\e[1S", to_key("#f4")); // + table_set(*key_map, "\e[1;1S", to_key("#f4")); // + table_set(*key_map, "\e[1;2S", to_key("#f4+$")); // + + + + + table_set(*key_map, "\e[1;3S", to_key("#f4+a")); // + + + + + table_set(*key_map, "\e[1;4S", to_key("#f4+A")); // + + + + + table_set(*key_map, "\e[1;5S", to_key("#f4+c")); // + + + + + table_set(*key_map, "\e[1;6S", to_key("#f4+C")); // + + + + + table_set(*key_map, "\e[1;7S", to_key("#f4+w")); // + + + + + table_set(*key_map, "\e[1;8S", to_key("#f4+W")); // + + + + + table_set(*key_map, "\e[1;9S", to_key("#f4+s")); // + + table_set(*key_map, "\e[1;10S", to_key("#f4+S")); // + + table_set(*key_map, "\e[1;11S", to_key("#f4+x")); // + + table_set(*key_map, "\e[1;12S", to_key("#f4+X")); // + + table_set(*key_map, "\e[1;13S", to_key("#f4+y")); // + + table_set(*key_map, "\e[1;14S", to_key("#f4+Y")); // + + table_set(*key_map, "\e[1;15S", to_key("#f4+z")); // + + table_set(*key_map, "\e[1;16S", to_key("#f4+Z")); // + + + // F5 // g i k l w x + table_set(*key_map, "\e[[E", to_key("#f5")); // + + table_set(*key_map, "\e[31~", to_key("#f5+$")); // + + table_set(*key_map, "\e[15~", to_key("#f5")); // + + + + + + table_set(*key_map, "\e[15;1~", to_key("#f5")); // + table_set(*key_map, "\e[15;2~", to_key("#f5+$")); // + + + + + + table_set(*key_map, "\e[15;3~", to_key("#f5+a")); // + + + + + + table_set(*key_map, "\e[15;4~", to_key("#f5+A")); // + + + + + + table_set(*key_map, "\e[15;5~", to_key("#f5+c")); // + + + + + + table_set(*key_map, "\e[15;6~", to_key("#f5+C")); // + + + + + + table_set(*key_map, "\e[15;7~", to_key("#f5+w")); // + + + + + + table_set(*key_map, "\e[15;8~", to_key("#f5+W")); // + + + + + + table_set(*key_map, "\e[15;9~", to_key("#f5+s")); // + + table_set(*key_map, "\e[15;10~",to_key("#f5+S")); // + + table_set(*key_map, "\e[15;11~",to_key("#f5+x")); // + + table_set(*key_map, "\e[15;12~",to_key("#f5+X")); // + + table_set(*key_map, "\e[15;13~",to_key("#f5+y")); // + + table_set(*key_map, "\e[15;14~",to_key("#f5+Y")); // + + table_set(*key_map, "\e[15;15~",to_key("#f5+z")); // + + table_set(*key_map, "\e[15;16~",to_key("#f5+Z")); // + + + // F6 // g i k l w x + table_set(*key_map, "\e[32~", to_key("#f6+$")); // + + table_set(*key_map, "\e[17~", to_key("#f6")); // + + + + + + + table_set(*key_map, "\e[17;1~", to_key("#f6")); // + table_set(*key_map, "\e[17;2~", to_key("#f6+$")); // + + + + + + table_set(*key_map, "\e[17;3~", to_key("#f6+a")); // + + + + + + table_set(*key_map, "\e[17;4~", to_key("#f6+A")); // + + + + + + table_set(*key_map, "\e[17;5~", to_key("#f6+c")); // + + + + + + table_set(*key_map, "\e[17;6~", to_key("#f6+C")); // + + + + + + table_set(*key_map, "\e[17;7~", to_key("#f6+w")); // + + + + + + table_set(*key_map, "\e[17;8~", to_key("#f6+W")); // + + + + + + table_set(*key_map, "\e[17;9~", to_key("#f6+s")); // + + table_set(*key_map, "\e[17;10~",to_key("#f6+S")); // + + table_set(*key_map, "\e[17;11~",to_key("#f6+x")); // + + table_set(*key_map, "\e[17;12~",to_key("#f6+X")); // + + table_set(*key_map, "\e[17;13~",to_key("#f6+y")); // + + table_set(*key_map, "\e[17;14~",to_key("#f6+Y")); // + + table_set(*key_map, "\e[17;15~",to_key("#f6+z")); // + + table_set(*key_map, "\e[17;16~",to_key("#f6+Z")); // + + + // F7 // g i k l w x + table_set(*key_map, "\e[33~", to_key("#f7+$")); // + + table_set(*key_map, "\e[18~", to_key("#f7")); // + + + + + + + table_set(*key_map, "\e[18;1~", to_key("#f7")); // + table_set(*key_map, "\e[18;2~", to_key("#f7+$")); // + + + + + + table_set(*key_map, "\e[18;3~", to_key("#f7+a")); // + + + + + + table_set(*key_map, "\e[18;4~", to_key("#f7+A")); // + + + + + + table_set(*key_map, "\e[18;5~", to_key("#f7+c")); // + + + + + + table_set(*key_map, "\e[18;6~", to_key("#f7+C")); // + + + + + + table_set(*key_map, "\e[18;7~", to_key("#f7+w")); // + + + + + + table_set(*key_map, "\e[18;8~", to_key("#f7+W")); // + + + + + + table_set(*key_map, "\e[18;9~", to_key("#f7+s")); // + + table_set(*key_map, "\e[18;10~",to_key("#f7+S")); // + + table_set(*key_map, "\e[18;11~",to_key("#f7+x")); // + + table_set(*key_map, "\e[18;12~",to_key("#f7+X")); // + + table_set(*key_map, "\e[18;13~",to_key("#f7+y")); // + + table_set(*key_map, "\e[18;14~",to_key("#f7+Y")); // + + table_set(*key_map, "\e[18;15~",to_key("#f7+z")); // + + table_set(*key_map, "\e[18;16~",to_key("#f7+Z")); // + + + // F8 // g i k l w x + table_set(*key_map, "\e[34~", to_key("#f8+$")); // + + table_set(*key_map, "\e[19~", to_key("#f8")); // + + + + + + + table_set(*key_map, "\e[19;1~", to_key("#f8")); // + table_set(*key_map, "\e[19;2~", to_key("#f8+$")); // + + + + + + table_set(*key_map, "\e[19;3~", to_key("#f8+a")); // + + + + + + table_set(*key_map, "\e[19;4~", to_key("#f8+A")); // + + + + + + table_set(*key_map, "\e[19;5~", to_key("#f8+c")); // + + + + + + table_set(*key_map, "\e[19;6~", to_key("#f8+C")); // + + + + + + table_set(*key_map, "\e[19;7~", to_key("#f8+w")); // + + + + + + table_set(*key_map, "\e[19;8~", to_key("#f8+W")); // + + + + + + table_set(*key_map, "\e[19;9~", to_key("#f8+s")); // + + table_set(*key_map, "\e[19;10~",to_key("#f8+S")); // + + table_set(*key_map, "\e[19;11~",to_key("#f8+x")); // + + table_set(*key_map, "\e[19;12~",to_key("#f8+X")); // + + table_set(*key_map, "\e[19;13~",to_key("#f8+y")); // + + table_set(*key_map, "\e[19;14~",to_key("#f8+Y")); // + + table_set(*key_map, "\e[19;15~",to_key("#f8+z")); // + + table_set(*key_map, "\e[19;16~",to_key("#f8+Z")); // + + + // F9 // g i k l w x + table_set(*key_map, "\e[20~", to_key("#f9")); // + + + + + + + table_set(*key_map, "\e[20;1~", to_key("#f9")); // + table_set(*key_map, "\e[20;2~", to_key("#f9+$")); // + + + + + + table_set(*key_map, "\e[20;3~", to_key("#f9+a")); // + + + + + + table_set(*key_map, "\e[20;4~", to_key("#f9+A")); // + + + + + + table_set(*key_map, "\e[20;5~", to_key("#f9+c")); // + + + + + + table_set(*key_map, "\e[20;6~", to_key("#f9+C")); // + + + + + + table_set(*key_map, "\e[20;7~", to_key("#f9+w")); // + + + + + + table_set(*key_map, "\e[20;8~", to_key("#f9+W")); // + + + + + + table_set(*key_map, "\e[20;9~", to_key("#f9+s")); // + + table_set(*key_map, "\e[20;10~",to_key("#f9+S")); // + + table_set(*key_map, "\e[20;11~",to_key("#f9+x")); // + + table_set(*key_map, "\e[20;12~",to_key("#f9+X")); // + + table_set(*key_map, "\e[20;13~",to_key("#f9+y")); // + + table_set(*key_map, "\e[20;14~",to_key("#f9+Y")); // + + table_set(*key_map, "\e[20;15~",to_key("#f9+z")); // + + table_set(*key_map, "\e[20;16~",to_key("#f9+Z")); // + + + // F10 // g i k l w x + table_set(*key_map, "\e[21~", to_key("#f10")); // + + + + + + + table_set(*key_map, "\e[21;1~", to_key("#f10")); // + table_set(*key_map, "\e[21;2~", to_key("#f10+$")); // + + + + + + table_set(*key_map, "\e[21;3~", to_key("#f10+a")); // + + + + + + table_set(*key_map, "\e[21;4~", to_key("#f10+A")); // + + + + + + table_set(*key_map, "\e[21;5~", to_key("#f10+c")); // + + + + + + table_set(*key_map, "\e[21;6~", to_key("#f10+C")); // + + + + + + table_set(*key_map, "\e[21;7~", to_key("#f10+w")); // + + + + + + table_set(*key_map, "\e[21;8~", to_key("#f10+W")); // + + + + + + table_set(*key_map, "\e[21;9~", to_key("#f10+s")); // + + table_set(*key_map, "\e[21;10~",to_key("#f10+S")); // + + table_set(*key_map, "\e[21;11~",to_key("#f10+x")); // + + table_set(*key_map, "\e[21;12~",to_key("#f10+X")); // + + table_set(*key_map, "\e[21;13~",to_key("#f10+y")); // + + table_set(*key_map, "\e[21;14~",to_key("#f10+Y")); // + + table_set(*key_map, "\e[21;15~",to_key("#f10+z")); // + + table_set(*key_map, "\e[21;16~",to_key("#f10+Z")); // + + + // F11 // g i k l w x + table_set(*key_map, "\e[23~", to_key("#f11")); // + + + + + + + table_set(*key_map, "\e[23;1~", to_key("#f11")); // + table_set(*key_map, "\e[23;2~", to_key("#f11+$")); // + + + + + + table_set(*key_map, "\e[23;3~", to_key("#f11+a")); // + + + + + + table_set(*key_map, "\e[23;4~", to_key("#f11+A")); // + + + + + + table_set(*key_map, "\e[23;5~", to_key("#f11+c")); // + + + + + + table_set(*key_map, "\e[23;6~", to_key("#f11+C")); // + + + + + + table_set(*key_map, "\e[23;7~", to_key("#f11+w")); // + + + + + + table_set(*key_map, "\e[23;8~", to_key("#f11+W")); // + + + + + + table_set(*key_map, "\e[23;9~", to_key("#f11+s")); // + + table_set(*key_map, "\e[23;10~",to_key("#f11+S")); // + + table_set(*key_map, "\e[23;11~",to_key("#f11+x")); // + + table_set(*key_map, "\e[23;12~",to_key("#f11+X")); // + + table_set(*key_map, "\e[23;13~",to_key("#f11+y")); // + + table_set(*key_map, "\e[23;14~",to_key("#f11+Y")); // + + table_set(*key_map, "\e[23;15~",to_key("#f11+z")); // + + table_set(*key_map, "\e[23;16~",to_key("#f11+Z")); // + + + // F12 // g i k l w x + table_set(*key_map, "\e[24~", to_key("#f12")); // + + + + + + + table_set(*key_map, "\e[24;1~", to_key("#f12")); // + table_set(*key_map, "\e[24;2~", to_key("#f12+$")); // + + + + + + table_set(*key_map, "\e[24;3~", to_key("#f12+a")); // + + + + + + table_set(*key_map, "\e[24;4~", to_key("#f12+A")); // + + + + + + table_set(*key_map, "\e[24;5~", to_key("#f12+c")); // + + + + + + table_set(*key_map, "\e[24;6~", to_key("#f12+C")); // + + + + + + table_set(*key_map, "\e[24;7~", to_key("#f12+w")); // + + + + + + table_set(*key_map, "\e[24;8~", to_key("#f12+W")); // + + + + + + table_set(*key_map, "\e[24;9~", to_key("#f12+s")); // + + table_set(*key_map, "\e[24;10~",to_key("#f12+S")); // + + table_set(*key_map, "\e[24;11~",to_key("#f12+x")); // + + table_set(*key_map, "\e[24;12~",to_key("#f12+X")); // + + table_set(*key_map, "\e[24;13~",to_key("#f12+y")); // + + table_set(*key_map, "\e[24;14~",to_key("#f12+Y")); // + + table_set(*key_map, "\e[24;15~",to_key("#f12+z")); // + + table_set(*key_map, "\e[24;16~",to_key("#f12+Z")); // + +} diff --git a/TUI/module.jai b/TUI/module.jai new file mode 100644 index 0000000..0563384 --- /dev/null +++ b/TUI/module.jai @@ -0,0 +1,817 @@ +/* + A simple terminal user interface module that provides basic functionalities similar to the [ncurses library](https://en.wikipedia.org/wiki/Ncurses). + Usefull for creating simple terminal-based apps that require user input. + View `snake.jai` for an example. + It has been tested on the following terminal emulators: + - [GNOME Terminal](https://en.wikipedia.org/wiki/GNOME_Terminal) + - [kitty](https://en.wikipedia.org/wiki/Kitty_(terminal_emulator)) + - [Konsole](https://en.wikipedia.org/wiki/Konsole) + - [Linux console](https://en.wikipedia.org/wiki/Linux_console) + - [xterm](https://en.wikipedia.org/wiki/Xterm) + - [Windows Terminal](https://en.wikipedia.org/wiki/Windows_Terminal) +*/ + +#module_parameters(COLOR_MODE_BITS := 24); + + +#scope_file + + +#if OS == { + case .LINUX; + #load "unix.jai"; + case .MACOS; + #load "unix.jai"; + case .WINDOWS; + #load "windows.jai"; + case; + #assert(false, "Unsupported OS."); +} + +#if COLOR_MODE_BITS == { + case 4; + #load "palette_4b.jai"; + case 8; + #load "palette_8b.jai"; + case 24; + #load "palette_24b.jai"; + case; + assert(false, "Invalid COLOR_MODE_BITS. Valid values are 4, 8, or 24 (default)."); +} + +#import "Basic"; +#import "String"; +#import "Thread"; +#import "UTF8"; +#load "key_map.jai"; + +#add_context tui_style : Style; // This contains the last style applied by the module. +#add_context tui_output_builder : *String_Builder; // If set, this will serve as an output buffer for this module procedures. + +KEY_SIZE :: #run type_info(Key).runtime_size; +#assert(input_buffer.count >= KEY_SIZE); // The input buffer size must be capable to hold an entire Key. + +active := false; +input_override : Key; +input_string : string; +input_buffer : [1024] u8; +temp_builder := String_Builder.{ allocator = temporary_allocator }; + + +#scope_module + + +assert_is_active :: inline () { + assert(active, "Please call setup_terminal() to start using this module."); +} + +log_tui_error :: (format_string: string, args: .. Any) { + write_strings(Commands.SaveCursorPosition, Commands.MainScreenBuffer); + log_error(format_string, ..args); + write_strings(Commands.AlternateScreenBuffer, Commands.RestoreCursorPosition); +} + + +#scope_export; + + +// Special Graphics Characters. +Drawings :: struct #type_info_none { + 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"; +} + +// Terminal Escape Codes. +Commands :: struct #type_info_none { + + // Screen buffers + AlternateScreenBuffer :: "\e[?1049h"; + MainScreenBuffer :: "\e[?1049l"; + + // Device. + Bell :: "\x07"; + QueryDeviceAttributes :: "\e[0c"; + + // Draw/text. + DrawingMode :: "\e(0"; + TextMode :: "\e(B"; + ClearToEndOfScreen :: "\e[0J"; // From current cursor position (inclusive) to end of screen. + ClearFromStartOfScreen :: "\e[1J"; // From start of screen to current cursor position. + ClearScreen :: "\e[2J"; // Leaves cursor in top left corner position. + ClearScrollBack :: "\e[3J"; + ClearToEndOfLine :: "\e[0K"; // From current cursor position (inclusive) to end of line. + ClearFromStartOfLine :: "\e[1K"; // From start of line to current cursor position. + ClearLine :: "\e[2K"; + SetGraphicsRendition :: "\e[%m"; + + // Text Modification. + InsertCharacters :: "\e[%@"; // Insert % spaces at curret cursor position (shifts existing text to the right). + DeleteCharacters :: "\e[%P"; // Delete % characters at the current cursor position (inserts space characters from the right). + EraseCharacters :: "\e[%X"; // Erase % characters from the current cursor position by overwriting them with space characters. + InsertLines :: "\e[%L"; // Insert % lines into the buffer at the current cursor position. + DeleteLines :: "\e[%M"; // Deletes % lines from the buffer, starting with the row the cursor is on. + + // Character encoding. + EncodingIEC2022 :: "\e%@"; + EncodingUTF8 :: "\e%G"; + + // Window. + SetWindowTitle :: "\e]0;%\e\\"; + RefreshWindow :: "\e[7t"; + QueryWindowSizeInChars :: "\e[18t"; + + // Cursor position. + SaveCursorPosition :: "\e7"; + RestoreCursorPosition :: "\e8"; + SetCursorPosition :: "\e[%;%H"; + QueryCursorPosition :: "\e[6n"; + + // Cursor visibility. + ShowCursor :: "\e[?25h"; + HideCursor :: "\e[?25l"; + StartBlinking :: "\e[?12h"; + StopBlinking :: "\e[?12l"; + + // 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"; +} + +Style :: struct { + #if COLOR_MODE_BITS == 4 || COLOR_MODE_BITS == 8 { + background: Palette; + foreground: Palette; + } else { + background: Color_24b; + foreground: Color_24b; + } + + background = Palette.BLACK; + foreground = Palette.WHITE; + + use_default_background_color := false; + use_default_foreground_color := false; + + bold: bool; + underline: bool; + strike_through: bool; + negative: bool; +} + +set_style :: (style: Style) { + // If no tui_output_builder is provided, use a temporary one and discard it afterwards. + builder := context.tui_output_builder; + temp_mark: Temporary_Storage_State = ---; + if context.tui_output_builder == null { + builder = *temp_builder; + temp_mark = get_temporary_storage_mark(); + } + + #if COLOR_MODE_BITS == { + case 4; + print_to_builder(builder, + #run sprint("%0%0", Commands.SetGraphicsRendition, Commands.SetGraphicsRendition), + cast(u8)style.foreground + 30, cast(u8)style.background + 40 + ); + + case 8; + print_to_builder(builder, + #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), + cast(u8)style.foreground, cast(u8)style.background + ); + + case 24; + print_to_builder(builder, + #run sprint(Commands.SetGraphicsRendition, "38;2;%;%;%;48;2;%;%;%"), + style.foreground.r, style.foreground.g, style.foreground.b, + style.background.r, style.background.g, style.background.b + ); + } + + if style.use_default_foreground_color { + append(builder, #run sprint(Commands.SetGraphicsRendition, "39")); + } + + if style.use_default_background_color { + append(builder, #run sprint(Commands.SetGraphicsRendition, "49")); + } + + if context.tui_output_builder == null { + write_builder(builder); + set_temporary_storage_mark(temp_mark); + } + + context.tui_style = style; +} + +clear_style :: () { + write_string(#run sprint(Commands.SetGraphicsRendition, "0")); + context.tui_style = .{ }; +} + +using_style :: (style: Style) #expand { + __style := context.tui_style; + set_style(style); + `defer set_style(__style); +} + + +//////////////////////////////////////////////////////////////////////////////// + +/* + 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; + +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"); +} + +to_key :: (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { + assert(str.count <= KEY_SIZE, "Invalid arguments passed to to_key(): 'str' has more than % bytes and cannot be stored as a Key.", KEY_SIZE); + + k: Key; + for 0..str.count-1 { + k |= ((cast(u64)str[it]) << (it*8)); + } + return k; +} + +to_string :: (key: Key) -> string { + str := alloc_string(KEY_SIZE); + str.count = 0; + while key != 0 { + str.count += 1; + str[str.count-1] = xx key & 0xFF; + key >>= 8; + } + return str; +} + +is_escape_code :: (key: Key) -> bool { + beginsWithEscape := ((key & 0xFF) ^ #char "#") == 0; + hasSomethingElse := (key & (~0xFF)) != 0; + return beginsWithEscape && hasSomethingElse; +} + +//////////////////////////////////////////////////////////////////////////////// + +is_active :: inline () -> bool { + return active; +} + +// Prepares the terminal to be used by the module. +setup_terminal :: () -> success := true #must { + if active == true return; + + input_string.data = input_buffer.data; + input_string.count = 0; + input_override = xx Keys.None; + + setup_key_map(); + + write_strings( + Commands.HideCursor, + Commands.SaveCursorPosition, + Commands.AlternateScreenBuffer, + Commands.EncodingUTF8, + Commands.CursorNormalMode, + Commands.KeypadNumMode + ); + + if !OS_prepare_terminal() then return false; + + active = true; + + return; +} + +// Restores the initial terminal settings. +reset_terminal :: () -> success := true #must { + if active == false return; + + active = false; + + clear_style(); + + if !OS_reset_terminal() then return false; + + write_strings( + Commands.MainScreenBuffer, + Commands.RestoreCursorPosition, + Commands.ShowCursor + ); + + reset_key_map(); + + return; +} + +set_next_key :: inline (key: Key) { + assert_is_active(); + input_override = key; +} + +// Returns, with the following priority: +// - last key passed to set_next_key; +// - Keys.Resize if terminal was resized; +// - key pressed by user; +// - Keys.None if everything else fails after the given timeout. +// If timeout is set to -1, it will wait indefinitely by the user input. +get_key :: (timeout_milliseconds: s32 = -1) -> Key { + assert_is_active(); + + if input_override != xx Keys.None { + defer input_override = xx Keys.None; + return input_override; + } + + if OS_was_terminal_resized() return Keys.Resize; + + // If there's nothing on the input_string buffer, await for input to be available, + // otherwise, if we have less than a complete Key, check if there's more to read. + should_read_input := false; + + if input_string.count == 0 { + should_read_input = OS_wait_for_input(timeout_milliseconds); + } + else if input_string.count < KEY_SIZE { + should_read_input = OS_wait_for_input(0); + } + + if should_read_input { + // Copy data to the start of the input_string buffer. + 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; + } + + // The terminal may have been resized while waiting for or reading the input; check it again. + if OS_was_terminal_resized() return Keys.Resize; + + 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_character_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 found, return the escape code, otherwise return a single escape character. + if success { + return key; + } + else { + to_parse.count = 1; + } + } + + return to_key(to_parse); +} + +// If count_limit has a non-negative value it will be used as the limit to the number of bytes on the returned string. +// If any ASCII characters are provided in the terminators list, they will be used to scan and interrupt the input, including +// the terminator as the last character. +// At least one of the arguments must be properly setup to avoid an infinite-loop reading the input. +read_input :: (count_limit: int = -1, terminators: .. u8) -> string { + assert_is_active(); + assert(count_limit >= 0 || terminators.count > 0, "Invalid arguments passed to read_input(): when 'count_limit' is less-than 0 (ignored), you need to provide 'terminators' to avoid an infinite-loop."); + + // Read until one of the terminator characters is found. + // Since we don't know the resulting size of the returned string, we must keep the string builder growing. + 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); + } + // Do the same but limit the number of bytes in the returned string. + 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; + } +} + +// Uses the get_key to read user input and show it on screen. +// Allows to move the cursor left and right and to delete/backspace. +// Enter ends the input, returning the input string and the Enter key. +// Escape discards the input returning an empty string and a Escape key. +// Resize discards the input returning an empty string and a Resize key. +read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { + assert_is_active(); + assert(count_limit >= 0, "Invalid arguments passed to read_input_line(): 'count_limit' must be greater-than or equal to 0."); + + // The returned memory must be allocated before we start to use temporary memory. + // Otherwise, the returned memory would be invalid on calls of type (,, temporary_allocator). + str := alloc_string(count_limit); + str.count = 0; + idx := 0; + + x, y := get_cursor_position(); + key := Keys.None; + + write_strings(Commands.ShowCursor, Commands.StartBlinking, Commands.BlinkingBarShape); + + while true { + builder := temp_builder; + + auto_release_temp(); + + chars_count := count_characters(str); + + // Preview input line. + if is_visible { + print_to_builder(*builder, Commands.SetCursorPosition, y, x); + append(*builder, str); + if count_limit > chars_count then print_to_builder(*builder, Commands.EraseCharacters, count_limit-chars_count); + } + else { + print_to_builder(*builder, Commands.SetCursorPosition, y, x); + for 1..chars_count append(*builder, "*"); + if count_limit > chars_count print_to_builder(*builder, Commands.EraseCharacters, count_limit-chars_count); + } + print_to_builder(*builder, Commands.SetCursorPosition, y, x+idx); + write_builder(*builder); + + // Process input key. + 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 < chars_count then idx += 1; + + case Keys.Home; + idx = 0; + + case Keys.End; + idx = chars_count; + + case Keys.Delete; + if idx == chars_count continue; + delete_character(*str, idx); + + case Keys.Backspace; + if idx == 0 continue; + idx -= 1; + delete_character(*str, idx); + + case; + if is_escape_code(key) continue; + + key_str := to_string(key,, allocator = temporary_allocator); + + // Get the buffer index to insert the next character. + buff_idx, success := get_byte_index(str, idx); + if success == false then buff_idx = str.count; + + // Make sure we have space to append the new character at the end (in case we're trying to do it). + if buff_idx > count_limit - key_str.count then continue; + + // Move text to allow inserting new character. + for < count_limit-1..buff_idx + key_str.count-1 { + str.data[it] = str.data[it-key_str.count]; + } + + memcpy(*str.data[buff_idx], key_str.data, key_str.count); + + if str.count < count_limit then str.count += key_str.count; + idx += 1; + + // Truncate string to avoid incomplete utf8 codes on the string tail. + str.count = truncate(str, count_limit).count; + } + } + + write_strings(Commands.StopBlinking, Commands.DefaultShape, Commands.HideCursor); + + result := ifx key == Keys.Enter then str else ""; + return result, key; +} + +flush_input :: () { + assert_is_active(); + OS_flush_input(); + input_string.data = input_buffer.data; + input_string.count = 0; +} + +draw_box :: (x: int, y: int, width: int, height: int, clear_inside := false) { + assert_is_active(); + assert(x > 0 && y > 0 && width > 1 && height > 1, "Invalid arguments passed to draw_box(): 'x' and 'y' must be greater-than 0; 'width' and 'height' must be greater-than 1."); + + // If no tui_output_builder is provided, use a temporary one and discard it afterwards. + builder := context.tui_output_builder; + temp_mark: Temporary_Storage_State = ---; + if context.tui_output_builder == null { + builder = *temp_builder; + temp_mark = get_temporary_storage_mark(); + } + + append(builder, Commands.DrawingMode); + + // Draw top line + print_to_builder(builder, Commands.SetCursorPosition, y, x); + append(builder, Drawings.CornerTL); + for 1..width-2 { + append(builder, Drawings.LineH); + } + append(builder, Drawings.CornerTR); + + // Draw left and right sides. + for idx: y+1..y+height-2 { + print_to_builder(builder, Commands.SetCursorPosition, idx, x); + append(builder, Drawings.LineV); + if clear_inside { + print_to_builder(builder, Commands.EraseCharacters, width-2); + } + print_to_builder(builder, Commands.SetCursorPosition, idx, x+width-1); + append(builder, Drawings.LineV); + } + + // Draw bottom line. + print_to_builder(builder, Commands.SetCursorPosition, y+height-1, x); + append(builder, Drawings.CornerBL); + for 1..width-2 { + append(builder, Drawings.LineH); + } + append(builder, Drawings.CornerBR); + + append(builder, Commands.TextMode); + + if context.tui_output_builder == null { + write_builder(builder); + set_temporary_storage_mark(temp_mark); + } +} + +clear_terminal :: inline () { + assert_is_active(); + write_string(Commands.ClearScreen); +} + +get_terminal_size :: () -> width: int, height: int { + assert_is_active(); + + auto_release_temp(); + + flush_input(); + write_string(Commands.QueryWindowSizeInChars); + + rows, columns: int = ---; + if OS_wait_for_input(1) { + + // 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",, allocator = 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], + "Failed to query window size: invalid response."); + + parts := split(input, ";",, allocator = temporary_allocator); + rows = parse_int(*parts[1]); + columns = parse_int(*parts[2]); + } + // Some systems don't allow to query the terminal size directly... or the answer takes too much time. + // In such cases, measure it indirectly by the maximum possible cursor position. + // (e.g.: allowWindowOps/disallowedWindowOps properties in xterm) + else { + write_string(Commands.SaveCursorPosition); + defer write_string(Commands.RestoreCursorPosition); + + set_cursor_position(0xFFFF, 0xFFFF,, tui_output_builder = null); + columns, rows = get_cursor_position(); + } + + return columns, rows; +} + +// Range between 1 and terminal size. +set_cursor_position :: inline (x: int, y: int) { + assert_is_active(); + if context.tui_output_builder == null { + print(Commands.SetCursorPosition, y, x); + } + else { + print_to_builder(context.tui_output_builder, Commands.SetCursorPosition, y, x); + } +} + +// Range between 1 and terminal size. +get_cursor_position :: () -> x: int, y: int { + assert_is_active(); + + 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",, allocator = temporary_allocator); + + // 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], + "Failed to query cursor position: invalid response."); + + advance(*input, 2); + parts := split(input, ";",, allocator = temporary_allocator); + row := parse_int(*parts[0]); + column := parse_int(*parts[1]); + return column, row; +} + +set_terminal_title :: inline (title: string) { + assert_is_active(); + print(Commands.SetWindowTitle, title); +} + +// Set the module's context string builder in the current scope context. +using_builder_as_output :: (builder: *String_Builder) #expand { + __builder := context.tui_output_builder; + context.tui_output_builder = builder; + `defer context.tui_output_builder = __builder; +} + +// Helper to use the module's context string builder. +tui_print :: inline (format_string: string, args: .. Any) { + if context.tui_output_builder == null { + print(format_string, ..args, to_standard_error = false); + } + else { + print_to_builder(context.tui_output_builder, format_string, ..args); + } +} + +// Helper to use the module's context string builder. +tui_write_string :: inline (s: string) { + if context.tui_output_builder == null { + write_string(s, to_standard_error = false); + } + else { + append(context.tui_output_builder, s); + } +} diff --git a/TUI/palette_24b.jai b/TUI/palette_24b.jai new file mode 100644 index 0000000..7545a0b --- /dev/null +++ b/TUI/palette_24b.jai @@ -0,0 +1,50 @@ +// https://www.ditig.com/publications/256-colors-cheat-sheet +Color_24b :: struct { + r: u8; + g: u8; + b: u8; +} + +Palette :: struct #type_info_none { + BLACK :: Color_24b.{0x00, 0x00, 0x00}; + MAROON :: Color_24b.{0x80, 0x00, 0x00}; + GREEN :: Color_24b.{0x00, 0x80, 0x00}; + OLIVE :: Color_24b.{0x80, 0x80, 0x00}; + NAVY :: Color_24b.{0x00, 0x00, 0x80}; + PURPLE :: Color_24b.{0x80, 0x00, 0x80}; + TEAL :: Color_24b.{0x00, 0x80, 0x80}; + SILVER :: Color_24b.{0xC0, 0xC0, 0xC0}; + GRAY :: Color_24b.{0x80, 0x80, 0x80}; + RED :: Color_24b.{0xFF, 0x00, 0x00}; + LIME :: Color_24b.{0x00, 0xFF, 0x00}; + YELLOW :: Color_24b.{0xFF, 0xFF, 0x00}; + BLUE :: Color_24b.{0x00, 0x00, 0xFF}; + MAGENTA :: Color_24b.{0xFF, 0x00, 0xFF}; + CYAN :: Color_24b.{0x00, 0xFF, 0xFF}; + WHITE :: Color_24b.{0xFF, 0xFF, 0xFF}; + + GRAY_3 :: Color_24b.{0x08, 0x08, 0x08}; + GRAY_7 :: Color_24b.{0x12, 0x12, 0x12}; + GRAY_10 :: Color_24b.{0x1C, 0x1C, 0x1C}; + GRAY_14 :: Color_24b.{0x26, 0x26, 0x26}; + GRAY_18 :: Color_24b.{0x30, 0x30, 0x30}; + GRAY_22 :: Color_24b.{0x3A, 0x3A, 0x3A}; + GRAY_26 :: Color_24b.{0x44, 0x44, 0x44}; + GRAY_30 :: Color_24b.{0x4E, 0x4E, 0x4E}; + GRAY_34 :: Color_24b.{0x58, 0x58, 0x58}; + GRAY_37 :: Color_24b.{0x62, 0x62, 0x62}; + GRAY_40 :: Color_24b.{0x6C, 0x6C, 0x6C}; + GRAY_46 :: Color_24b.{0x76, 0x76, 0x76}; + GRAY_50 :: GRAY; + GRAY_54 :: Color_24b.{0x8A, 0x8A, 0x8A}; + GRAY_58 :: Color_24b.{0x94, 0x94, 0x94}; + GRAY_61 :: Color_24b.{0x9E, 0x9E, 0x9E}; + GRAY_65 :: Color_24b.{0xA8, 0xA8, 0xA8}; + GRAY_69 :: Color_24b.{0xB2, 0xB2, 0xB2}; + GRAY_73 :: Color_24b.{0xBC, 0xBC, 0xBC}; + GRAY_77 :: Color_24b.{0xC6, 0xC6, 0xC6}; + GRAY_81 :: Color_24b.{0xD0, 0xD0, 0xD0}; + GRAY_85 :: Color_24b.{0xDA, 0xDA, 0xDA}; + GRAY_89 :: Color_24b.{0xE4, 0xE4, 0xE4}; + GRAY_93 :: Color_24b.{0xEE, 0xEE, 0xEE}; +} diff --git a/TUI/palette_4b.jai b/TUI/palette_4b.jai new file mode 100644 index 0000000..b0317d2 --- /dev/null +++ b/TUI/palette_4b.jai @@ -0,0 +1,19 @@ +// https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit +Palette :: enum u8 { + BLACK :: 0; + MAROON :: 1; + GREEN :: 2; + OLIVE :: 3; + NAVY :: 4; + PURPLE :: 5; + TEAL :: 6; + SILVER :: 7; + GRAY :: 60; + RED :: 61; + LIME :: 62; + YELLOW :: 63; + BLUE :: 64; + MAGENTA :: 65; + CYAN :: 66; + WHITE :: 67; +} diff --git a/TUI/palette_8b.jai b/TUI/palette_8b.jai new file mode 100644 index 0000000..36a512f --- /dev/null +++ b/TUI/palette_8b.jai @@ -0,0 +1,307 @@ +// https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit +Palette :: enum u8 { + 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; + + + x000000 :: 16; + x00005F :: 17; + x000087 :: 18; + x0000AF :: 19; + x0000D7 :: 20; + x0000FF :: 21; + + x005F00 :: 22; + x005F5F :: 23; + x005F87 :: 24; + x005FAF :: 25; + x005FD7 :: 26; + x005FFF :: 27; + + x008700 :: 28; + x00875F :: 29; + x008787 :: 30; + x0087AF :: 31; + x0087D7 :: 32; + x0087FF :: 33; + + x00AF00 :: 34; + x00AF5F :: 35; + x00AF87 :: 36; + x00AFAF :: 37; + x00AFD7 :: 38; + x00AFFF :: 39; + + x00D700 :: 40; + x00D75F :: 41; + x00D787 :: 42; + x00D7AF :: 43; + x00D7D7 :: 44; + x00D7FF :: 45; + + x00FF00 :: 46; + x00FF5F :: 47; + x00FF87 :: 48; + x00FFAF :: 49; + x00FFD7 :: 50; + x00FFFF :: 51; + + + x5F0000 :: 52; + x5F005F :: 53; + x5F0087 :: 54; + x5F00AF :: 55; + x5F00D7 :: 56; + x5F00FF :: 57; + + x5F5F00 :: 58; + x5F5F5F :: 59; + x5F5F87 :: 60; + x5F5FAF :: 61; + x5F5FD7 :: 62; + x5F5FFF :: 63; + + x5F8700 :: 64; + x5F875F :: 65; + x5F8787 :: 66; + x5F87AF :: 67; + x5F87D7 :: 68; + x5F87FF :: 69; + + x5FAF00 :: 70; + x5FAF5F :: 71; + x5FAF87 :: 72; + x5FAFAF :: 73; + x5FAFD7 :: 74; + x5FAFFF :: 75; + + x5FD700 :: 76; + x5FD75F :: 77; + x5FD787 :: 78; + x5FD7AF :: 79; + x5FD7D7 :: 80; + x5FD7FF :: 81; + + x5FFF00 :: 82; + x5FFF5F :: 83; + x5FFF87 :: 84; + x5FFFAF :: 85; + x5FFFD7 :: 86; + x5FFFFF :: 87; + + + x870000 :: 88; + x87005F :: 89; + x870087 :: 90; + x8700AF :: 91; + x8700D7 :: 92; + x8700FF :: 93; + + x875F00 :: 94; + x875F5F :: 95; + x875F87 :: 96; + x875FAF :: 97; + x875FD7 :: 98; + x875FFF :: 99; + + x878700 :: 100; + x87875F :: 101; + x878787 :: 102; + x8787AF :: 103; + x8787D7 :: 104; + x8787FF :: 105; + + x87AF00 :: 106; + x87AF5F :: 107; + x87AF87 :: 108; + x87AFAF :: 109; + x87AFD7 :: 110; + x87AFFF :: 111; + + x87D700 :: 112; + x87D75F :: 113; + x87D787 :: 114; + x87D7AF :: 115; + x87D7D7 :: 116; + x87D7FF :: 117; + + x87FF00 :: 118; + x87FF5F :: 119; + x87FF87 :: 120; + x87FFAF :: 121; + x87FFD7 :: 122; + x87FFFF :: 123; + + + xAF0000 :: 124; + xAF005F :: 125; + xAF0087 :: 126; + xAF00AF :: 127; + xAF00D7 :: 128; + xAF00FF :: 129; + + xAF5F00 :: 130; + xAF5F5F :: 131; + xAF5F87 :: 132; + xAF5FAF :: 133; + xAF5FD7 :: 134; + xAF5FFF :: 135; + + xAF8700 :: 136; + xAF875F :: 137; + xAF8787 :: 138; + xAF87AF :: 139; + xAF87D7 :: 140; + xAF87FF :: 141; + + xAFAF00 :: 142; + xAFAF5F :: 143; + xAFAF87 :: 144; + xAFAFAF :: 145; + xAFAFD7 :: 146; + xAFAFFF :: 147; + + xAFD700 :: 148; + xAFD75F :: 149; + xAFD787 :: 150; + xAFD7AF :: 151; + xAFD7D7 :: 152; + xAFD7FF :: 153; + + xAFFF00 :: 154; + xAFFF5F :: 155; + xAFFF87 :: 156; + xAFFFAF :: 157; + xAFFFD7 :: 158; + xAFFFFF :: 159; + + + xD70000 :: 160; + xD7005F :: 161; + xD70087 :: 162; + xD700AF :: 163; + xD700D7 :: 164; + xD700FF :: 165; + + xD75F00 :: 166; + xD75F5F :: 167; + xD75F87 :: 168; + xD75FAF :: 169; + xD75FD7 :: 170; + xD75FFF :: 171; + + xD78700 :: 172; + xD7875F :: 173; + xD78787 :: 174; + xD787AF :: 175; + xD787D7 :: 176; + xD787FF :: 177; + + xD7AF00 :: 178; + xD7AF5F :: 179; + xD7AF87 :: 180; + xD7AFAF :: 181; + xD7AFD7 :: 182; + xD7AFFF :: 183; + + xD7D700 :: 184; + xD7D75F :: 185; + xD7D787 :: 186; + xD7D7AF :: 187; + xD7D7D7 :: 188; + xD7D7FF :: 189; + + xD7FF00 :: 190; + xD7FF5F :: 191; + xD7FF87 :: 192; + xD7FFAF :: 193; + xD7FFD7 :: 194; + xD7FFFF :: 195; + + + xFF0000 :: 196; + xFF005F :: 197; + xFF0087 :: 198; + xFF00AF :: 199; + xFF00D7 :: 200; + xFF00FF :: 201; + + xFF5F00 :: 202; + xFF5F5F :: 203; + xFF5F87 :: 204; + xFF5FAF :: 205; + xFF5FD7 :: 206; + xFF5FFF :: 207; + + xFF8700 :: 208; + xFF875F :: 209; + xFF8787 :: 210; + xFF87AF :: 211; + xFF87D7 :: 212; + xFF87FF :: 213; + + xFFAF00 :: 214; + xFFAF5F :: 215; + xFFAF87 :: 216; + xFFAFAF :: 217; + xFFAFD7 :: 218; + xFFAFFF :: 219; + + xFFD700 :: 220; + xFFD75F :: 221; + xFFD787 :: 222; + xFFD7AF :: 223; + xFFD7D7 :: 224; + xFFD7FF :: 225; + + xFFFF00 :: 226; + xFFFF5F :: 227; + xFFFF87 :: 228; + xFFFFAF :: 229; + xFFFFD7 :: 230; + xFFFFFF :: 231; + + + // Grayscale + x080808 :: 232; + x121212 :: 233; + x1C1C1C :: 234; + x262626 :: 235; + x303030 :: 236; + x3A3A3A :: 237; + + x444444 :: 238; + x4E4E4E :: 239; + x585858 :: 240; + x636363 :: 241; + x6C6C6C :: 242; + x767676 :: 243; + + x808080 :: 244; + x8A8A8A :: 245; + x949494 :: 246; + x9E9E9E :: 247; + xA8A8A8 :: 248; + xB2B2B2 :: 249; + + xBCBCBC :: 250; + xC6C6C6 :: 251; + xD0D0D0 :: 252; + xDADADA :: 253; + xE4E4E4 :: 254; + xEEEEEE :: 255; +} diff --git a/TUI/tests.jai b/TUI/tests.jai new file mode 100644 index 0000000..a740e6b --- /dev/null +++ b/TUI/tests.jai @@ -0,0 +1,232 @@ +// build: jai -import_dir ../ tests.jai +#import "Basic"; +TUI :: #import "TUI"; + +main :: () { + + assert_result :: (result: bool, error_message: string) { + if result == true { + print("- success\n", to_standard_error = true); + } + else { + assert(TUI.reset_terminal(), "Failed to reset TUI."); + print("- ERROR: %", error_message, to_standard_error = true); + exit(1); + } + } + + next_line :: inline () { + x, y := TUI.get_cursor_position(); + TUI.set_cursor_position(1, y+1); + } + + if 1 { + print("TEST : set and get cursor position\n", to_standard_error = true); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + X :: 2; + Y :: 3; + TUI.set_cursor_position(X, Y); + x, y := TUI.get_cursor_position(); + assert(TUI.reset_terminal(), "Failed to reset TUI."); + assert_result(x == X && y == Y, "Failed set/get cursor position.\n"); + } + + if 1 { + print("TEST : test key input\n", to_standard_error = true); + auto_release_temp(); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.clear_terminal(); + TUI.set_cursor_position(1, 1); + write_string("Press q to exit, other key to print it to screen, wait 1s to see animation."); + next_line(); + key: TUI.Key; + while(key != #char "q") { + key = TUI.get_key(1000); + if key == TUI.Keys.None { + write_string("-"); + } + else if key == TUI.Keys.Resize { + write_string("#"); + } + else { + // else if key >= 32 && key <= 128 then print_character(cast,force(u8)key) + write_string(TUI.to_string(key)); + } + } + assert(TUI.reset_terminal(), "Failed to reset TUI."); + print("- success\n", to_standard_error = true); + } + + if 1 { + print("TEST : draw box\n", to_standard_error = true); + auto_release_temp(); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.flush_input(); + TUI.clear_terminal(); + TUI.draw_box(1, 2, 5, 3); + TUI.set_cursor_position(1, 1); + print("Can you see the box below? (y/n)"); + key := TUI.get_key(); + assert(TUI.reset_terminal(), "Failed to reset TUI."); + assert_result(key == #char "y", "Failed to draw box.\n"); + } + + if 1 { + print("TEST : get terminal size\n", to_standard_error = true); + auto_release_temp(); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.clear_terminal(); + width, height := TUI.get_terminal_size(); + TUI.set_cursor_position(1, 1); + print("Is terminal size %x%? (y/n)", width, height); + key: TUI.Key = xx TUI.Keys.None; + while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { + key = TUI.get_key(); + } + assert(TUI.reset_terminal(), "Failed to reset TUI."); + assert_result(key == #char "y", "Failed to get terminal size.\n"); + } + + if 1 { + print("TEST : set terminal title\n", to_standard_error = true); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + title := "BAZINGA"; + TUI.set_terminal_title(title); + TUI.set_cursor_position(1, 1); + print("Is terminal title '%'? (y/n)", title); + key: TUI.Key = xx TUI.Keys.None; + while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { + key = TUI.get_key(); + } + assert(TUI.reset_terminal(), "Failed to reset TUI."); + assert_result(key == #char "y", "Failed to set terminal title.\n"); + } + + if 1 { + print("TEST : print keys\n", to_standard_error = true); + auto_release_temp(); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + key: TUI.Key = TUI.Keys.None; + last_none_char := "X"; + + width, height := TUI.get_terminal_size(); + TUI.clear_terminal(); + TUI.draw_box(1, 1, width, height); + drop_down := 0; + + while(key != #char "q") { + + if key == { + case TUI.Keys.None; { + TUI.set_cursor_position(2, 2); + last_none_char = ifx last_none_char == "X" then "+" else "X"; + write_strings(last_none_char, " (press: q to exit, c to clear, and any other key to print it's value)"); + } + + case TUI.Keys.Resize; #through; + case #char "c"; { + width, height = TUI.get_terminal_size(); + TUI.clear_terminal(); + TUI.draw_box(1, 1, width, height); + drop_down = 0; + } + + case; { + TUI.set_cursor_position(2, 3+drop_down); + str := TUI.to_string(key); + array_to_print: [..] string; + write_string(": "); + for 0..str.count-1 { + print("% ", FormatInt.{value = cast(u8)str[it], base=16}); + } + write_string(": "); + for 0..str.count-1 { + if str[it] == #char "\e" { + str[it] = #char "#"; + } + } + write_string(str); + write_string(" :"); + drop_down += 1; + } + } + + x := ifx width > 24 then width-24 else 1; + y := ifx height > 1 then height-1 else 1; + + TUI.set_cursor_position(x, y); + print("size = %x%\n", width, height); + key = TUI.get_key(1000); + + // __mark := get_temporary_storage_mark(); + // set_temporary_storage_mark(__mark); + } + print("- success"); + assert(TUI.reset_terminal(), "Failed to reset TUI."); + } + + if 1 { + print("TEST : user input\n", to_standard_error = true); + auto_release_temp(); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.clear_terminal(); + TUI.set_cursor_position(1, 1); + print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); + next_line(); + str, key := TUI.read_input_line(15); + TUI.set_cursor_position(1, 3); + error_message: string; + if key == { + case TUI.Keys.Escape; { + print("Have you pressed Esc? (y/n)"); + error_message = "Failed to read line on Esc."; + } + + case TUI.Keys.Resize; { + print("Have you resized the terminal? (y/n)"); + error_message = "Failed to read line on resize."; + + } + case; { + print("Have you entered '%'? (y/n)", str); + error_message = "Failed to read line."; + } + } + answer := TUI.get_key(); + assert(TUI.reset_terminal(), "Failed to reset TUI."); + assert_result(answer == #char "y", error_message); + } + + if 1 { + print("TEST : hidden user input\n", to_standard_error = true); + auto_release_temp(); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.clear_terminal(); + TUI.set_cursor_position(1, 1); + print("Enter some secret (use Enter to finish, Esc to cancel, or resize to abort):"); + next_line(); + str, key := TUI.read_input_line(15, false); + TUI.set_cursor_position(1, 3); + error_message: string; + if key == { + case TUI.Keys.Escape; { + print("Have you pressed Esc? (y/n)"); + error_message = "Failed to read line on Esc."; + } + + case TUI.Keys.Resize; { + print("Have you resized the terminal? (y/n)"); + error_message = "Failed to read line on resize."; + } + case; { + print("Have you entered '%'? (y/n)", str); + error_message = "Failed to read line."; + } + } + answer := TUI.get_key(); + assert(TUI.reset_terminal(), "Failed to reset TUI."); + assert_result(answer == #char "y", error_message); + } + + // -- -- -- Testing TUI -- STOP +} diff --git a/TUI/unix.jai b/TUI/unix.jai new file mode 100644 index 0000000..99cc61d --- /dev/null +++ b/TUI/unix.jai @@ -0,0 +1,319 @@ +#scope_file + +#import "Atomics"; +#import "System"; +#import "POSIX"; + + // Queue selector used in tcflush(...). + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-struct.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Queue_Selector :: enum s32 { + #if OS == { + case .LINUX; + TCIFLUSH :: 0; // Discard data received but not yet read. + TCOFLUSH :: 1; // Discard data written but not yet sent. + TCIOFLUSH :: 2; // Discard all pending data. + + case .MACOS; + TCIFLUSH :: 1; // Discard data received but not yet read. + TCOFLUSH :: 2; // Discard data written but not yet sent. + TCIOFLUSH :: 3; // Discard all pending data. + } + } + + // Optional actions used in tcsetattr(...). + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-tcflow.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Optional_Actions :: enum s32 { + TCSANOW :: 0; // Change immediately. + TCSADRAIN :: 1; // Change when pending output is written. + TCSAFLUSH :: 2; // Flush pending input before changing. + } + + // Terminal control (struct termios). + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-struct.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Terminal_IO_Mode :: struct { + + #if OS == { + case .LINUX; + NCCS :: 32; + + case .MACOS; + NCCS :: 20; + } + + c_iflag : Input_Modes; // Input mode flags. + c_oflag : Output_Modes; // Output mode flags. + c_cflag : Control_Modes; // Control modes flags. + c_lflag : Local_Modes; // Local modes flags. + c_line : u8; // Line discipline. + c_cc : [NCCS]Control_Chars; // Control characters. + c_ispeed : u32; // Input speed (baud rates). + c_ospeed : u32; // Output speed (baud rates). + } + + // Input modes. + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-c_iflag.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Input_Modes :: enum_flags u32 { + IGNBRK :: 0x00000001; // Ignore break condition. + BRKINT :: 0x00000002; // Signal interrupt on break. + IGNPAR :: 0x00000004; // Ignore characters with parity errors. + PARMRK :: 0x00000008; // Mark parity and framing errors. + INPCK :: 0x00000010; // Enable input parity check. + ISTRIP :: 0x00000020; // Strip 8th bit off characters. + INLCR :: 0x00000040; // Map NL to CR on input. + IGNCR :: 0x00000080; // Ignore CR. + ICRNL :: 0x00000100; // Map CR to NL on input. + + #if OS == { + + case .LINUX; + IXON :: 0x00000400; // Enable start/stop output control. + IXANY :: 0x00000800; // Any character will restart after stop. + IXOFF :: 0x00001000; // Enable start/stop input control. + + case .MACOS; + IXON :: 0x00000200; // Enable start/stop output control. + IXANY :: 0x00000400; // Any character will restart after stop. + IXOFF :: 0x00000800; // Enable start/stop input control. + } + } + + // Output modes. + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-c_oflag.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Output_Modes :: enum_flags u32 { + #if OS == { + + case .LINUX; + OPOST :: 0x00000001; // Perform output processing. + ONLCR :: 0x00000004; // Map NL to CR-NL on output. + OCRNL :: 0x00000008; // Map CR to NL. + ONOCR :: 0x00000010; // Discard CR's when on column 0. + ONLRET :: 0x00000020; // Move to column 0 on NL. + OFILL :: 0x00000040; // Send fill characters for delays. + + case .MACOS; + OPOST :: 0x00000001; // Perform output processing. + ONLCR :: 0x00000002; // Map NL to CR-NL on output. + OCRNL :: 0x00000010; // Map CR to NL. + ONOCR :: 0x00000020; // Discard CR's when on column 0. + ONLRET :: 0x00000040; // Move to column 0 on NL. + OFILL :: 0x00000080; // Send fill characters for delays. + } + } + + // Control modes. + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-c_cflag.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Control_Modes :: enum u32 { + #if OS == { + + case .LINUX; + CS5 :: 0x00000000; // 5 bits per byte. + CS6 :: 0x00000010; // 6 bits per byte. + CS7 :: 0x00000020; // 7 bits per byte. + CS8 :: 0x00000030; // 8 bits per byte. + CSIZE :: 0x00000030; // Number of bits per byte (mask). + CSTOPB :: 0x00000040; // Two stop bits instead of one. + CREAD :: 0x00000080; // Enable receiver. + PARENB :: 0x00000100; // Parity enable. + PARODD :: 0x00000200; // Odd parity instead of even. + HUPCL :: 0x00000400; // Hang up on last close. + CLOCAL :: 0x00000800; + + case .MACOS; + CS5 :: 0x00000000; // 5 bits per byte. + CS6 :: 0x00000100; // 6 bits per byte. + CS7 :: 0x00000200; // 7 bits per byte. + CS8 :: 0x00000300; // 8 bits per byte. + CSIZE :: 0x00000300; // Number of bits per byte (mask). + CSTOPB :: 0x00000400; // Two stop bits instead of one. + CREAD :: 0x00000800; // Enable receiver. + PARENB :: 0x00001000; // Parity enable. + PARODD :: 0x00002000; // Odd parity instead of even. + HUPCL :: 0x00004000; // Hang up on last close. + CLOCAL :: 0x00008000; + } + } + + // Local modes. + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-c_lflag.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Local_Modes :: enum_flags u32 { + #if OS == { + + case .LINUX; + ISIG :: 0x00000001; // Enable signals. + ICANON :: 0x00000002; // Canonical input (erase and kill processing). + ECHO :: 0x00000008; // Enable echo. + ECHOE :: 0x00000010; // Visual erase for ERASE. + ECHOK :: 0x00000020; // Echo NL after KILL. + ECHONL :: 0x00000040; // Echo NL even if ECHO is off. + NOFLSH :: 0x00000080; // Disable flush after interrupt or quit. + TOSTOP :: 0x00000100; // Send SIGTTOU for background output. + IEXTEN :: 0x00008000; // Enable DISCARD and LNEXT. + + case .MACOS; + ISIG :: 0x00000080; // Enable signals INTR, QUIT, [D]SUSP. + ICANON :: 0x00000100; // Canonicalize input lines. + ECHO :: 0x00000008; // Enable echo. + ECHOE :: 0x00000002; // Visual erase for ERASE. + ECHOK :: 0x00000004; // Echo NL after KILL. + ECHONL :: 0x00000010; // Echo NL even if ECHO is off. + NOFLSH :: 0x80000000; // Disable flush after interrupt. + TOSTOP :: 0x00400000; // Stop background jobs from output. + IEXTEN :: 0x00000400; // Enable DISCARD and LNEXT. + } + } + + // Control Characters + // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-c_cc.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + Control_Chars :: enum u8 { + // Unused consts: + // VINTR, VQUIT, VERASE, VKILL, VEOF, VSWTC, VSTART, VSTOP, VSUSP, VEOL, VREPRINT, VDISCARD, VWERASE, VLNEXT, VEOL2 + + #if OS == { + + case .LINUX; + VTIME :: 5; // Time-out value (tenths of a second) [!ICANON]. + VMIN :: 6; // Minimum number of bytes read at once [!ICANON]. + + case .MACOS; + VTIME :: 17; // Time-out value (tenths of a second) [!ICANON]. + VMIN :: 16; // Minimum number of bytes read at once [!ICANON]. + } + } + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcsetattr.c.html + tcsetattr :: (fd: s32, optional_actions: s32, termios_p : *Terminal_IO_Mode) -> s32 #foreign libc; + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html + tcgetattr :: (fd: s32, termios_p: *Terminal_IO_Mode) -> s32 #foreign libc; + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcflush.c.html + tcflush :: (fd: s32, queue_selector: s32) -> s32 #foreign libc; + +//////////////////////////////////////////////////////////////////////////////// + + initial_tio_mode: Terminal_IO_Mode; + raw_tio_mode: Terminal_IO_Mode; + + was_resized : bool; + +//////////////////////////////////////////////////////////////////////////////// + +resize_handler :: (signal_code : s32) #c_call { + new_context : Context; + push_context new_context { + if signal_code != SIGWINCH then return; + atomic_swap(*was_resized, true); + } +} + +prepare_resize_handler :: () { + sa : sigaction_t; + sa.sa_handler = resize_handler; + sigemptyset(*(sa.sa_mask)); + sa.sa_flags = SA_RESTART; + sigaction(SIGWINCH, *sa, null); +} + +restore_resize_handler :: () { + sa : sigaction_t; + sa.sa_handler = SIG_DFL; + sigaction(SIGWINCH, null, *sa); +} + +//////////////////////////////////////////////////////////////////////////////// + +#scope_module + +OS_prepare_terminal :: () -> success := true { + error: int = ---; + + error = tcgetattr(STDIN_FILENO, *initial_tio_mode); + if error { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to get initial_tio_mode: code %, %", error_code, error_string); + return false; + } + + raw_tio_mode = initial_tio_mode; + raw_tio_mode.c_iflag &= ~(.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); + raw_tio_mode.c_oflag &= ~(.OPOST); + raw_tio_mode.c_lflag &= ~(.ECHO | .ECHONL | .ICANON | .ISIG | .IEXTEN); + raw_tio_mode.c_cflag &= ~(.CSIZE | .PARENB); + raw_tio_mode.c_cflag |= .CS8; + raw_tio_mode.c_cc[Control_Chars.VMIN] = 1; + raw_tio_mode.c_cc[Control_Chars.VTIME] = 0; + + error = tcsetattr(STDIN_FILENO, xx Optional_Actions.TCSANOW, *raw_tio_mode); + if error { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to set raw_tio_mode: code %, %", error_code, error_string); + return false; + } + + was_resized = false; + prepare_resize_handler(); + return; +} + +OS_reset_terminal :: () -> success := true { + restore_resize_handler(); + error := tcsetattr(STDIN_FILENO, xx Optional_Actions.TCSANOW, *initial_tio_mode); + if error { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to set initial_tio_mode: code %, %", error_code, error_string); + return false; + } + return; +} + +OS_flush_input :: () -> success := true { + error := tcflush(STDIN_FILENO, xx Queue_Selector.TCIFLUSH); + if error { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to flush input: code %, %", error_code, error_string); + return false; + } + return; +} + +OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success := true { + bytes_read := read(STDIN_FILENO, buffer, xx bytes_to_read); + if bytes_read < 0 { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to read input: code %, %", error_code, error_string); + return 0, false; + } + return bytes_read; +} + +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool, success := true { + fds := pollfd.[ .{ fd = STDIN_FILENO, events = POLLIN, revents = 0 } ]; + nfds := fds.count; + result := poll(fds.data, xx nfds, xx timeout_milliseconds); // Returns '-1' with errno '4 | Interrupted system call' on window resize. + + if result == -1 { + error_code, error_string := get_error_value_and_string(); + // Ignore window resize events (error_code 4). + if error_code != 4 { + log_tui_error("Unexpected error while waiting for input: code %, %", error_code, error_string); + return false, false; + } + } + + return ifx result > 0 then true else false; +} + +OS_was_terminal_resized :: inline () -> bool { + return atomic_swap(*was_resized, false); +} diff --git a/TUI/windows.jai b/TUI/windows.jai new file mode 100644 index 0000000..f8d8bc8 --- /dev/null +++ b/TUI/windows.jai @@ -0,0 +1,390 @@ +#scope_file + +#import "Basic"; +#import "System"; +#import "Windows"; +#import "Windows_Utf8"; + + // Code page identifiers. + CP_UTF8 :: 65001; + + // https://learn.microsoft.com/windows/win32/winprog/windows-data-types + LPVOID :: *void; + BOOL :: bool; + CHAR :: s8; + WCHAR :: s16; + SHORT :: s16; + WORD :: u16; + DWORD :: u32; + LPDWORD :: *u32; + UINT :: u32; + PINPUT_RECORD :: *INPUT_RECORD; + + // https://learn.microsoft.com/en-us/windows/console/setconsolemode + // https://learn.microsoft.com/en-us/windows/console/high-level-console-modes + Console_Input_Mode :: enum_flags u32 { + ENABLE_PROCESSED_INPUT; // If set, control sequences are processed by the system. + ENABLE_LINE_INPUT; // If set, ReadFile and ReadConsole function return on CR; otherwise return when characters are available. + ENABLE_ECHO_INPUT; // If set, Echoes input on screen. Only available if ENABLE_LINE_INPUT is set. + ENABLE_WINDOW_INPUT; + ENABLE_MOUSE_INPUT; + ENABLE_INSERT_MODE; + _UNUSED_0040_; + _UNUSED_0080_; + _UNUSED_0100_; + ENABLE_VIRTUAL_TERMINAL_INPUT; // If set, makes user input available to the ReadConsole function. + _UNUSED_0400_; + _UNUSED_0800_; + } + + // https://learn.microsoft.com/en-us/windows/console/setconsolemode + // https://learn.microsoft.com/en-us/windows/console/high-level-console-modes + Console_Output_Mode :: enum_flags u32 { + ENABLE_PROCESSED_OUTPUT; // If set, ASCII control sequences are processed by the system. + ENABLE_WRAP_AT_EOL_OUTPUT; // If set, the cursor moves to the beginning of the next row when it reaches the end of the current row. + ENABLE_VIRTUAL_TERMINAL_PROCESSING; // If set, VT100 control sequences are processed by the system. + DISABLE_NEWLINE_AUTO_RETURN; + ENABLE_LVB_GRID_WORLDWIDE; + _UNUSED_0020_; + _UNUSED_0040_; + _UNUSED_0080_; + } + + COORD :: struct { + X : SHORT; + Y : SHORT; + } + + INPUT_RECORD_EVENT_TYPE :: enum u16 { + KEY_EVENT :: 0x0001; + MOUSE_EVENT :: 0x0002; + WINDOW_BUFFER_SIZE_EVENT :: 0x0004; + MENU_EVENT :: 0x0008; + FOCUS_EVENT :: 0x0010; + } + + INPUT_RECORD :: struct { + EventType : INPUT_RECORD_EVENT_TYPE; + union { + KeyEvent : KEY_EVENT_RECORD; + MouseEvent : MOUSE_EVENT_RECORD; + WindowBufferSizeEvent : WINDOW_BUFFER_SIZE_RECORD; + // These events are used internally and should be ignored. + MenuEvent : MENU_EVENT_RECORD; + FocusEvent : FOCUS_EVENT_RECORD; + } + } + + KEY_EVENT_RECORD :: struct { + bKeyDown : BOOL; + wRepeatCount : WORD #align 4; + wVirtualKeyCode : WORD; + wVirtualScanCode : WORD; + union { + UnicodeChar : WCHAR; + AsciiChar : CHAR; + } + dwControlKeyState : DWORD; + } + + MOUSE_EVENT_RECORD :: struct { + dwMousePosition : COORD; + dwButtonState : DWORD; + dwControlKeyState : DWORD; + dwEventFlags : DWORD; + } + + WINDOW_BUFFER_SIZE_RECORD :: struct { + dwSize : COORD; + } + + MENU_EVENT_RECORD :: struct { + dwCommandId : UINT; + } + + FOCUS_EVENT_RECORD :: struct { + bSetFocus : BOOL; + } + + // https://learn.microsoft.com/en-us/windows/console/readconsoleinput + ReadConsoleInputW :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> success: bool #foreign kernel32; + + // https://learn.microsoft.com/windows/console/readconsole + ReadConsoleW :: (hConsoleInput: HANDLE, lpBuffer: LPVOID, nNumberOfchars_to_read: DWORD, lpNumberOfchars_read: LPVOID, pInputControl: LPVOID) -> success: bool #foreign kernel32; + + // https://learn.microsoft.com/windows/console/flushconsoleinputbuffer + FlushConsoleInputBuffer :: (hConsoleInput: HANDLE) -> bool #foreign kernel32; + + // https://learn.microsoft.com/windows/console/getnumberofconsoleinputevents + GetNumberOfConsoleInputEvents :: (hConsoleInput: HANDLE, lpcNumberOfEvents: LPDWORD) -> bool #foreign kernel32; + + // https://learn.microsoft.com/en-us/windows/console/peekconsoleinput + PeekConsoleInputW :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> bool #foreign kernel32; + +//////////////////////////////////////////////////////////////////////////////// + + stdin: HANDLE; + initial_stdin_mode: u32; + raw_stdin_mode: Console_Input_Mode; + + stdout: HANDLE; + initial_stdout_mode: u32; + raw_stdout_mode: Console_Output_Mode; + + was_resized: bool; + + widechar_buffer: [512] u16; + +//////////////////////////////////////////////////////////////////////////////// + +peek_input :: () -> INPUT_RECORD, success := true { + record: INPUT_RECORD; + records_read: u32; + if PeekConsoleInputW(stdin, *record, 1, *records_read) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to peek input: code %, %", error_code, error_string); + return record, false; + } + return record; +} + +read_input :: () -> INPUT_RECORD, success := true { + record: INPUT_RECORD; + records_read: u32; + if ReadConsoleInputW(stdin, *record, 1, *records_read) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to read input: code %, %", error_code, error_string); + return record, false; + } + return record; +} + +count_input :: () -> u32, success := true { + count: u32; + if GetNumberOfConsoleInputEvents(stdin, *count) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to count input: code %, %", error_code, error_string); + return 0, false; + } + return count; +} + +//////////////////////////////////////////////////////////////////////////////// + +#scope_module + +OS_prepare_terminal :: () -> success := true { + + // stdin + stdin = GetStdHandle(STD_INPUT_HANDLE); + if stdin == INVALID_HANDLE_VALUE { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Invalid input handler: code %, %", error_code, error_string); + return false; + } + if xx GetConsoleMode(stdin, *initial_stdin_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to get input mode: code %, %", error_code, error_string); + return false; + } + raw_stdin_mode = (cast(Console_Input_Mode) initial_stdin_mode); + raw_stdin_mode |= (.ENABLE_VIRTUAL_TERMINAL_INPUT); + raw_stdin_mode &= ~(.ENABLE_LINE_INPUT | .ENABLE_PROCESSED_INPUT | .ENABLE_ECHO_INPUT); + + if xx SetConsoleMode(stdin, xx raw_stdin_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to set input mode: code %, %", error_code, error_string); + return false; + } + + // stdout + stdout = GetStdHandle(STD_OUTPUT_HANDLE); + if stdout == INVALID_HANDLE_VALUE { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Invalid output handler: code %, %", error_code, error_string); + return false; + } + if xx GetConsoleMode(stdout, *initial_stdout_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to get output mode: code %, %", error_code, error_string); + return false; + } + raw_stdout_mode = (cast(Console_Output_Mode) initial_stdout_mode); + raw_stdout_mode |= (.ENABLE_VIRTUAL_TERMINAL_PROCESSING | .ENABLE_PROCESSED_OUTPUT | .ENABLE_WRAP_AT_EOL_OUTPUT); + + if xx SetConsoleMode(stdout, xx raw_stdout_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to set output mode: code %, %", error_code, error_string); + return false; + } + + // Acording to [documentation](https://learn.microsoft.com/en-us/windows/win32/intl/code-pages) + // only the ANSI functions (ending in A) use the Code Page info. The Unicode functions (ending in W) + // already handle Unicode text. + // As long we use the Unicode functions, we shouldn't need to set the code page to UTF8. + // SetConsoleCP(CP_UTF8); + // SetConsoleOutputCP(CP_UTF8); + + return; +} + +OS_reset_terminal :: () -> success := true { + if xx SetConsoleMode(stdin, initial_stdin_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to reset input mode: code %, %", error_code, error_string); + return false; + } + if xx SetConsoleMode(stdout, initial_stdout_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to reset output mode: code %, %", error_code, error_string); + return false; + } + return; +} + +OS_flush_input :: () { + // This API is not recommended and does not have a virtual terminal equivalent. + // Attempting to empty the input queue all at once can destroy state in the queue in an unexpected manner. + if FlushConsoleInputBuffer(stdin) == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to flush input: code %, %", error_code, error_string); + } +} + +OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success := true { + + S32_MAX :: 0x7fff_ffff; + if (bytes_to_read > S32_MAX) { + log_tui_error("The Windows API only allows to read up to 2^32 bytes from the standard input. Clamping input argument."); + bytes_to_read = S32_MAX; + } + + success: bool; + bytes_read: s64 = 0; + available_inputs:, success = count_input(); + + while bytes_to_read > 0 && available_inputs > 0 { + + record := peek_input(); + + if record.EventType == { + case .WINDOW_BUFFER_SIZE_EVENT; + was_resized = true; + read_input(); + available_inputs -= 1; + + case .KEY_EVENT; + if record.KeyEvent.bKeyDown == false { + read_input(); + available_inputs -= 1; + } + else { + + widechar_view: [] u16; + widechar_view.data = widechar_buffer.data; + + chars_to_read := ifx available_inputs <= widechar_buffer.count then available_inputs else widechar_buffer.count; + + success = ReadConsoleW(stdin, widechar_view.data, chars_to_read, *widechar_view.count, null); + if success == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to read console: code %, %", error_code, error_string); + return 0, false; + } + + result:, success = wide_to_utf8_new(widechar_view.data, xx widechar_view.count); + if success == false { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Failed to convert from wide to UTF8: code %, %", error_code, error_string); + return 0, false; + } + + memcpy(*buffer[bytes_read], result.data, result.count); + + bytes_to_read -= xx result.count; + bytes_read += result.count; + + } + + // Discard other input events. + case; + read_input(); + + } + available_inputs = count_input(); + } + return bytes_read; +} + +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool, success := true { + // The Windows API provides all input events (keyboard, mouse, window resize) on a single input buffer. + // To make it match this module's API, we need to do some pre-processing while waiting for input. + // This means that OS_wait_for_input will peek at the input events, signal if a window resize is found, + // and discard unwanted events (like button release events). + // A similar logic is applied in OS_read_input. + + expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0); + + // Possible values for poll_return: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject + WAIT_OBJECT_0 :: 0x00000000; // Detected input. + WAIT_TIMEOUT :: 0x00000102; // Reached timeout. + WAIT_FAILED :: 0xFFFFFFFF; // Something went wrong. + + while true { + wait_result := WaitForSingleObject(stdin, cast,no_check(u32)timeout_milliseconds); + + if wait_result == WAIT_FAILED { + error_code, error_string := get_error_value_and_string(); + log_tui_error("Error while waiting for input: code %, %", error_code, error_string); + return false, false; + } + + if wait_result != WAIT_OBJECT_0 then return false; + + // Discard invalid input events. + count := count_input(); + while count > 0 { + record := peek_input(); + + if record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + // Discard any additional resize event. + while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { + read_input(); + } + + was_resized = true; + return false; + } + + if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true { + return true; + } + + read_input(); + count -= 1; + } + + // When waiting indefinitely... just continue. + if timeout_milliseconds < 0 then continue; + + // Either break due to timeout, or update the remaining timeout. + now := current_time_monotonic(); + if now >= expiration then break; + timeout_milliseconds = xx to_milliseconds(expiration - now); + } + + return false; +} + +OS_was_terminal_resized :: inline () -> bool { + while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; + read_input(); + } + + defer was_resized = false; + return was_resized; +} -- cgit v1.2.3