aboutsummaryrefslogtreecommitdiff
path: root/TUI
diff options
context:
space:
mode:
Diffstat (limited to 'TUI')
-rw-r--r--TUI/examples/snake.jai188
-rw-r--r--TUI/key_map.jai510
-rw-r--r--TUI/module.jai817
-rw-r--r--TUI/palette_24b.jai50
-rw-r--r--TUI/palette_4b.jai19
-rw-r--r--TUI/palette_8b.jai307
-rw-r--r--TUI/tests.jai232
-rw-r--r--TUI/unix.jai319
-rw-r--r--TUI/windows.jai390
9 files changed, 2832 insertions, 0 deletions
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;<r>;<c>t
+ // where <r> is the number of rows and <c> of columns.
+ FORMAT :: "\e[8;<r>;<c>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>;<c>R
+ // where <r> is the number of rows and <c> of columns.
+ FORMAT :: "\e[<r>;<c>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;
+}