aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--LICENSE-ISC15
-rw-r--r--LICENSE-MIT21
-rw-r--r--README.md29
-rw-r--r--Saturation/module.jai418
-rw-r--r--Saturation/tests.jai647
-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
-rw-r--r--UTF8/module.jai149
-rw-r--r--UTF8/tests.jai162
17 files changed, 4274 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..30bcfa4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.build/
diff --git a/LICENSE-ISC b/LICENSE-ISC
new file mode 100644
index 0000000..3ca0ef1
--- /dev/null
+++ b/LICENSE-ISC
@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2024 Daniel Almeida Martins
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/LICENSE-MIT b/LICENSE-MIT
new file mode 100644
index 0000000..1632077
--- /dev/null
+++ b/LICENSE-MIT
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Daniel Almeida Martins
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b41dc9f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+Readme
+======
+Modules for the language being developed by Thekla, Inc.
+
+## Saturation
+This module provides basic integer [saturation arithmetic](https://en.wikipedia.org/wiki/Saturation_arithmetic) procedures: `add`, `sub`, `mul`, and `div`.
+These procedures accept any of the built-in integer types and adjust the output accordingly, e.g., adding an `u8` with an `s16` results in an `s16`;
+All procedures return a flag signaling if the result is saturated and, additionally, the division procedure returns the remainder;
+Branch-free procedures are included for the x64 architecture. These may be used by setting the `PREFER_BRANCH_FREE_CODE` module argument, or by setting `prefer_branch_free_code` on each function call. These should speed things up, specially when using signed values. Some benchmarks are included on the tests file.
+
+## TUI
+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)
+
+## UTF8
+Basic operations over UTF8 encoded strings.
+
+# License
+Licensed under MIT or ISC.
+
+SPDX-License-Identifier: MIT OR ISC
diff --git a/Saturation/module.jai b/Saturation/module.jai
new file mode 100644
index 0000000..50c9b3c
--- /dev/null
+++ b/Saturation/module.jai
@@ -0,0 +1,418 @@
+// Integer saturating arighmetic (with assembly branch-free procedures for x64 - expecting signed values in two's complement).
+
+#module_parameters(PREFER_BRANCH_FREE_CODE := false);
+
+#import "Basic";
+#import "Math";
+#import "String";
+
+
+INTEGER_ARITHMETIC_TYPES_CHECK :: #string DONE
+ type_info_x := cast(*Type_Info)Tx;
+ type_info_y := cast(*Type_Info)Ty;
+ if type_info_x.type != .INTEGER || type_info_y.type != .INTEGER return false, "Non integers values passed.";
+ tx := cast(*Type_Info_Integer)type_info_x;
+ ty := cast(*Type_Info_Integer)type_info_y;
+
+ largest_type :=
+ ifx tx.runtime_size > ty.runtime_size then Tx else
+ ifx ty.runtime_size > tx.runtime_size then Ty else
+ ifx tx.signed == ty.signed then Tx else
+ void;
+
+ // Only allow to add different signedness values if largest type is the signed one (as in JAI).
+ if tx.signed == ty.signed {
+ Tx = largest_type;
+ Ty = largest_type;
+ Tr = largest_type;
+ }
+ else if tx.signed && Tx == largest_type {
+ Ty = largest_type;
+ Tr = largest_type;
+ }
+ else if ty.signed && Ty == largest_type {
+ Tx = largest_type;
+ Tr = largest_type;
+ }
+ else return false, "Number signedness mismatch.";
+
+ return true;
+DONE
+
+add :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; }
+{
+
+ #if !(prefer_branch_free_code && CPU == .X64) {
+
+ #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 {
+
+ #if Tr == s8 { MAX :: S8_MAX; MIN :: S8_MIN; }
+ #if Tr == s16 { MAX :: S16_MAX; MIN :: S16_MIN; }
+ #if Tr == s32 { MAX :: S32_MAX; MIN :: S32_MIN; }
+ #if Tr == s64 { MAX :: S64_MAX; MIN :: S64_MIN; }
+
+ if (y > 0 && x > MAX - y) then return MAX, true;
+ if (y < 0 && x < MIN - y) then return MIN, true;
+
+ } else {
+
+ #if Tr == u8 { MAX :: U8_MAX; }
+ #if Tr == u16 { MAX :: U16_MAX; }
+ #if Tr == u32 { MAX :: U32_MAX; }
+ #if Tr == u64 { MAX :: U64_MAX; }
+
+ if (x > MAX - y) then return MAX, true;
+
+ }
+
+ return x + y, false;
+
+ } else {
+
+ result: Tr = ---;
+ saturated: bool = ---;
+
+
+ ADD_SIGNED_ASM :: #string DONE
+ #asm {
+ mov result, -1; // Pre-set result with signed maximum (set all bits...
+ shr.SIZE result, 1; // ...then, clear MSB).
+ bt x, SIGN_BIT; // Test sign bit (affect CF).
+ adc result, 0; // Overflow signed maximum to signed minimum if CF is set.
+
+ add.SIZE x, y; // Add values (affect OF).
+ seto saturated; // Set saturated flag if OF.
+ cmovno result, x; // Move add-result to result if NOT OF.
+ }
+ DONE
+
+ #if Tr == s8
+ #insert #run replace(replace(ADD_SIGNED_ASM, ".SIZE", ".b"), "SIGN_BIT", "7");
+ #if Tr == s16
+ #insert #run replace(replace(ADD_SIGNED_ASM, ".SIZE", ".w"), "SIGN_BIT", "15");
+ #if Tr == s32
+ #insert #run replace(replace(ADD_SIGNED_ASM, ".SIZE", ".d"), "SIGN_BIT", "31");
+ #if Tr == s64
+ #insert #run replace(replace(ADD_SIGNED_ASM, ".SIZE", ".q"), "SIGN_BIT", "63");
+
+
+ ADD_UNSIGNED_ASM :: #string DONE
+ #asm {
+ mov result, -1; // Pre-set result with unsigned maximum.
+ add.SIZE x, y; // Add values (affect CF).
+ setc saturated; // Set saturated flag if CF.
+ cmovnc result, x; // Move add-result to result if NOT CF.
+ }
+ DONE
+
+ #if Tr == u8
+ #insert #run replace(ADD_UNSIGNED_ASM, ".SIZE", ".b");
+ #if Tr == u16
+ #insert #run replace(ADD_UNSIGNED_ASM, ".SIZE", ".w");
+ #if Tr == u32
+ #insert #run replace(ADD_UNSIGNED_ASM, ".SIZE", ".d");
+ #if Tr == u64
+ #insert #run replace(ADD_UNSIGNED_ASM, ".SIZE", ".q");
+
+
+ return result, saturated;
+
+ }
+}
+
+sub :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; }
+{
+
+ #if !(prefer_branch_free_code && CPU == .X64) {
+
+ #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 {
+
+ #if Tr == s8 { MAX :: S8_MAX; MIN :: S8_MIN; }
+ #if Tr == s16 { MAX :: S16_MAX; MIN :: S16_MIN; }
+ #if Tr == s32 { MAX :: S32_MAX; MIN :: S32_MIN; }
+ #if Tr == s64 { MAX :: S64_MAX; MIN :: S64_MIN; }
+
+ if (y < 0 && x > MAX + y) then return MAX, true;
+ if (y > 0 && x < MIN + y) then return MIN, true;
+
+ } else {
+
+ if (y > x) then return 0, true;
+
+ }
+
+ return x - y, false;
+
+ } else {
+
+ result: Tr = ---;
+ saturated: bool = ---;
+
+
+ SUB_SIGNED_ASM :: #string DONE
+ #asm {
+ mov result, -1; // Pre-set result with signed maximum (set all bits...
+ shr.SIZE result, 1; // ...then, clear MSB).
+ bt x, SIGN_BIT; // Test signal bit (affect CF).
+ adc result, 0; // Overflow signed maximum to signed minimum if CF is set.
+
+ sub.SIZE x, y; // Subtract values (affect OF).
+ seto saturated; // Set saturated flag if OF.
+ cmovno result, x; // Move subtract-result to result if NOT OF.
+ }
+ DONE
+
+ #if Tr == s8
+ #insert #run replace(replace(SUB_SIGNED_ASM, ".SIZE", ".b"), "SIGN_BIT", "7");
+ #if Tr == s16
+ #insert #run replace(replace(SUB_SIGNED_ASM, ".SIZE", ".w"), "SIGN_BIT", "15");
+ #if Tr == s32
+ #insert #run replace(replace(SUB_SIGNED_ASM, ".SIZE", ".d"), "SIGN_BIT", "31");
+ #if Tr == s64
+ #insert #run replace(replace(SUB_SIGNED_ASM, ".SIZE", ".q"), "SIGN_BIT", "63");
+
+
+ SUB_UNSIGNED_ASM :: #string DONE
+ #asm {
+ xor result, result; // Pre-set result with usigned minimum (zero).
+ sub.SIZE x, y; // Subtract values (affect CF).
+ setc saturated; // Set saturated flag if CF.
+ cmovnc result, x; // Move subtract-result to result if NOT CF.
+ }
+ DONE
+
+ #if Tr == u8
+ #insert #run replace(SUB_UNSIGNED_ASM, ".SIZE", ".b");
+ #if Tr == u16
+ #insert #run replace(SUB_UNSIGNED_ASM, ".SIZE", ".w");
+ #if Tr == u32
+ #insert #run replace(SUB_UNSIGNED_ASM, ".SIZE", ".d");
+ #if Tr == u64
+ #insert #run replace(SUB_UNSIGNED_ASM, ".SIZE", ".q");
+
+
+ return result, saturated;
+
+ }
+
+}
+
+mul :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; }
+{
+
+ #if !(prefer_branch_free_code && CPU == .X64) {
+
+ #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 {
+
+ #if Tr == s8 { MAX :: S8_MAX; MIN :: S8_MIN; }
+ #if Tr == s16 { MAX :: S16_MAX; MIN :: S16_MIN; }
+ #if Tr == s32 { MAX :: S32_MAX; MIN :: S32_MIN; }
+ #if Tr == s64 { MAX :: S64_MAX; MIN :: S64_MIN; }
+
+ if x == 0 || y == 0 then return 0, false;
+ if x > 0 && y > 0 && x > MAX / y then return MAX, true;
+ if x < 0 && y < 0 && x < MAX / y then return MAX, true;
+ if (y < 0 && x > 0 && y < MIN / x) || (x < 0 && y > 0 && x < MIN / y) then return MIN, true;
+
+ } else {
+
+ #if Tr == u8 { MAX :: U8_MAX; }
+ #if Tr == u16 { MAX :: U16_MAX; }
+ #if Tr == u32 { MAX :: U32_MAX; }
+ #if Tr == u64 { MAX :: U64_MAX; }
+
+ if x == 0 || y == 0 then return 0, false;
+ if x > MAX / y then return MAX, true;
+
+ }
+
+ return x * y, false;
+
+ } else {
+
+ result: Tr = ---;
+ saturated: bool = ---;
+
+ MUL_SIGNED_ASM :: #string DONE
+ #asm {
+ // Using two copies of the x value (x_, sign) seems to be a bit faster (not sure why).
+ mov x_: gpr === a, x; // Pin copy of x value to register A.
+
+ mov result, -1; // Pre-set result with signed maximum (set all bits...
+ shr.SIZE result, 1; // ...then, clear MSB).
+ mov sign:, x; // Use copy of x value.
+ xor sign, y; // Calculate result signal bit using xor.
+ bt sign, SIGN_BIT; // Test signal bit (affect CF).
+ adc result, 0; // Overflow signed maximum to signed minimum if CF is set.
+
+ imul.SIZE x_, y; // Multiply values (affect OF).
+ seto saturated; // Set saturated flag if OF.
+ cmovno result, x_; // Move multiply-result to result if NOT OF.
+ }
+ DONE
+
+ #if Tr == s8
+ #insert #run replace(replace(MUL_SIGNED_ASM, ".SIZE", ".b"), "SIGN_BIT", "7");
+ #if Tr == s16
+ #insert #run replace(replace(MUL_SIGNED_ASM, ".SIZE", ".w"), "SIGN_BIT", "15");
+ #if Tr == s32
+ #insert #run replace(replace(MUL_SIGNED_ASM, ".SIZE", ".d"), "SIGN_BIT", "31");
+ #if Tr == s64
+ #insert #run replace(replace(MUL_SIGNED_ASM, ".SIZE", ".q"), "SIGN_BIT", "63");
+
+
+ MUL_UNSIGNED_ASM :: #string DONE
+ #asm {
+ result === a; // Pin result to register A.
+
+ mov result, x; // Move value x to result.
+ mul.SIZE reg_d:, result, y; // Multiply values (affect CF).
+ setc saturated; // Set saturated flag if CF.
+ sbb mask:, mask; // If CF: mask = -1 (all bits set); else: mask = 0.
+ or result, mask; // If CF was set, then result will be set to unsigned maximum (all bits set).
+ }
+ DONE
+
+ #if Tr == u8
+ #insert #run replace(replace(MUL_UNSIGNED_ASM, ".SIZE", ".b"), "reg_d:,", ""); // For 8bits mul, we do not need D register.
+ #if Tr == u16
+ #insert #run replace(MUL_UNSIGNED_ASM, ".SIZE", ".w");
+ #if Tr == u32
+ #insert #run replace(MUL_UNSIGNED_ASM, ".SIZE", ".d");
+ #if Tr == u64
+ #insert #run replace(MUL_UNSIGNED_ASM, ".SIZE", ".q");
+
+
+ return result, saturated;
+
+ }
+}
+
+div :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, remainder: Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; }
+{
+
+ #if !(prefer_branch_free_code && CPU == .X64) {
+
+ #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 {
+
+ #if Tr == s8 { MAX :: S8_MAX; MIN :: S8_MIN; }
+ #if Tr == s16 { MAX :: S16_MAX; MIN :: S16_MIN; }
+ #if Tr == s32 { MAX :: S32_MAX; MIN :: S32_MIN; }
+ #if Tr == s64 { MAX :: S64_MAX; MIN :: S64_MIN; }
+
+ if x == MIN && y == -1 then return MAX, -1, true;
+
+ }
+
+ result := x / y;
+ remainder := x - (y * result);
+ return result, remainder, false;
+
+ } else {
+
+ result: Tr = ---;
+ remainder: Tr = ---;
+ saturated: bool = ---;
+
+ DIV_SIGNED_ASM :: #string DONE
+ #asm {
+ result === a; // Pin result to register A (to be used as dividend on idiv).
+ remainder === d; // Pin remainder to register D.
+
+ xor saturated, saturated; // Clear saturated.
+
+ // Detect div(MIN/-1) and flag it on ZF.
+ mov t_dividend:, -1; // Pre-set t_dividend with signed minimum (set all bits...
+ shr.SIZE t_dividend, 1; // ...then, clear MSB...
+ not t_dividend; // ...then, negate to obtain MSB set and all other bits cleared).
+ //
+ mov limit:, t_dividend; // Keep copy of signed minimum on limit.
+ add limit, 1; // Set limit as signed minimum + 1.
+ //
+ xor.SIZE t_dividend, x; // Clear dividend if x value is equal to signed minimum.
+ //
+ mov t_divisor:, -1; // Pre-set test_divisor with -1.
+ xor.SIZE t_divisor, y; // Clear test_divisor if y value is equal to -1.
+ //
+ or.SIZE t_dividend, t_divisor; // Or t_dividend with t_divisor (affect ZF).
+
+ setz saturated; // Set saturated flag if ZF.
+ mov result, x; // Copy x value to result (dividend).
+ cmovz result, limit; // If ZF: copy limit (signed minimum + 1) to result (dividend).
+
+ DIVIDE_PLACEHOLDER
+
+ sub.SIZE remainder, saturated; // If saturated: remainder = 0 - 1; otherwise: remainder = x - 0.
+ }
+ DONE
+
+ DIV_SIGNED_CALC_8BITS :: #string DONE
+ cbw result; // Prepare dividend high bits (sign-extend).
+ idiv.SIZE result, y; // Divide values.
+ mov remainder, result; // Extract remainder from result's high bits.
+ sar remainder, 8; // Shift remainder from high to low bits.
+ DONE
+
+ DIV_SIGNED_CALC_16BITS :: #string DONE
+ cwd remainder, result; // Prepare dividend high bits (sign-extend).
+ idiv.SIZE remainder, result, y; // Divide values.
+ DONE
+
+ DIV_SIGNED_CALC_32BITS :: #string DONE
+ cdq remainder, result; // Prepare dividend high bits (sign-extend).
+ idiv.SIZE remainder, result, y; // Divide values.
+ DONE
+
+ DIV_SIGNED_CALC_64BITS :: #string DONE
+ cqo remainder, result; // Prepare dividend high bits (sign-extend).
+ idiv.SIZE remainder, result, y; // Divide values.
+ DONE
+
+ #if Tr == s8
+ #insert #run replace(replace(DIV_SIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_SIGNED_CALC_8BITS), ".SIZE", ".b");
+ #if Tr == s16
+ #insert #run replace(replace(DIV_SIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_SIGNED_CALC_16BITS), ".SIZE", ".w");
+ #if Tr == s32
+ #insert #run replace(replace(DIV_SIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_SIGNED_CALC_32BITS), ".SIZE", ".d");
+ #if Tr == s64
+ #insert #run replace(replace(DIV_SIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_SIGNED_CALC_64BITS), ".SIZE", ".q");
+
+
+ DIV_UNSIGNED_ASM :: #string DONE
+ #asm {
+ result === a; // Pin result to register A.
+ remainder === d; // Pin remainder to register D.
+
+ xor result, result; // Clear result.
+ xor remainder, remainder; // Clear remainder (required when used as dividend's high bits).
+ xor saturated, saturated; // Clear saturated (unsigned division never saturates).
+ mov.SIZE result, x; // Copy x value to result.
+
+ DIVIDE_PLACEHOLDER
+ }
+ DONE
+
+ DIV_UNSIGNED_CALC_8BITS :: #string DONE
+ div.SIZE result, y; // Divide values.
+ mov remainder, result; // Extract remainder from result's high bits.
+ sar remainder, 8; // Shift remainder from high to low bits.
+ DONE
+
+ DIV_UNSIGNED_CALC :: #string DONE
+ div.SIZE remainder, result, y; // Divide values.
+ DONE
+
+ #if Tr == u8
+ #insert #run replace(replace(DIV_UNSIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_UNSIGNED_CALC_8BITS), ".SIZE", ".b");
+ #if Tr == u16
+ #insert #run replace(replace(DIV_UNSIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_UNSIGNED_CALC), ".SIZE", ".w");
+ #if Tr == u32
+ #insert #run replace(replace(DIV_UNSIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_UNSIGNED_CALC), ".SIZE", ".d");
+ #if Tr == u64
+ #insert #run replace(replace(DIV_UNSIGNED_ASM, "DIVIDE_PLACEHOLDER", DIV_UNSIGNED_CALC), ".SIZE", ".q");
+
+
+ return result, remainder, saturated;
+
+ }
+}
diff --git a/Saturation/tests.jai b/Saturation/tests.jai
new file mode 100644
index 0000000..2a82300
--- /dev/null
+++ b/Saturation/tests.jai
@@ -0,0 +1,647 @@
+// Tests for integer saturating arighmetic procedures.
+
+AVOID_INFINITE_FOR_LOOP :: true;
+
+#import "Basic";
+#import "Compiler";
+#import "Math";
+#import "String";
+#import "Saturation";
+
+main :: () {
+
+ write_strings(
+ "#=======================#\n",
+ "# Basic tests #\n"
+ );
+
+
+ test_op :: ($operation: string, x: $Tx, y: $Ty, result: $Tr, type: Type, saturated: bool, remainder: Tr = 0) -> errors_found: int #expand {
+
+ #insert #run () -> string {
+ // Build test call.
+ builder: String_Builder;
+ call := ifx operation == "div"
+ then "t_result, t_remainder, t_saturated := %(cast(Tx)x, cast(Ty)y);"
+ else "t_result, t_saturated := %(cast(Tx)x, cast(Ty)y);";
+
+ print(*builder, call, operation);
+
+ return builder_to_string(*builder);
+ }();
+
+ errors := 0;
+ log: String_Builder;
+ if result != t_result { errors += 1; print(*log, " > incorrect result value: got % expected %\n", t_result, result); };
+ if type != type_of(t_result) { errors += 1; print(*log, " > incorrect result type: got % expected %\n", type_of(t_result), type); };
+ if saturated != t_saturated { errors += 1; print(*log, " > incorrect saturated flag: got % expected %\n", t_saturated, saturated); };
+ #if operation == "div" {
+ if remainder != t_remainder { errors += 1; print(*log, " > incorrect remainder value: got % expected %\n", t_remainder, remainder); };
+ }
+
+ if errors > 0 {
+ #if operation == "div" {
+ print("%_%(%, %) = % + %0%0\n", operation, type, x, y, result, remainder, ifx saturated then " : saturated");
+ }
+ else {
+ print("%_%(%, %) = %0%0\n", operation, type, x, y, result, ifx saturated then " : saturated");
+ }
+ write_builder(*log);
+ }
+
+ return errors;
+ }
+
+ errors := 0;
+
+ // Test signed add.
+ errors += test_op("add", cast( s8) S8_MAX, cast( s8)1, S8_MAX, s8, true);
+ errors += test_op("add", cast(s16)S16_MAX, cast( u8)1, S16_MAX, s16, true);
+ errors += test_op("add", cast(s32)S32_MAX, cast(s32)1, S32_MAX, s32, true);
+ errors += test_op("add", cast(s64)S64_MAX, cast(u32)1, S64_MAX, s64, true);
+
+ errors += test_op("add", cast( s8) S8_MAX, cast( s8) S8_MIN, -1, s8, false);
+ errors += test_op("add", cast(s16)S16_MAX, cast(s16)S16_MIN, -1, s16, false);
+ errors += test_op("add", cast(s32)S32_MAX, cast(s32)S32_MIN, -1, s32, false);
+ errors += test_op("add", cast(s64)S64_MAX, cast(s64)S64_MIN, -1, s64, false);
+
+ // Test unsigned add.
+ errors += test_op("add", cast( u8) U8_MAX, cast( u8)1, U8_MAX, u8, true);
+ errors += test_op("add", cast(u16)U16_MAX, cast(u16)1, U16_MAX, u16, true);
+ errors += test_op("add", cast(u32)U32_MAX, cast(u32)1, U32_MAX, u32, true);
+ errors += test_op("add", cast(u64)U64_MAX, cast(u64)1, U64_MAX, u64, true);
+
+ errors += test_op("add", cast( u8) U8_MAX, cast( u8)0, U8_MAX, u8, false);
+ errors += test_op("add", cast(u16)U16_MAX, cast(u16)0, U16_MAX, u16, false);
+ errors += test_op("add", cast(u32)U32_MAX, cast(u32)0, U32_MAX, u32, false);
+ errors += test_op("add", cast(u64)U64_MAX, cast(u64)0, U64_MAX, u64, false);
+
+ // Test signed sub.
+ errors += test_op("sub", cast( s8) S8_MIN, cast( s8)1, S8_MIN, s8, true);
+ errors += test_op("sub", cast(s16)S16_MIN, cast( u8)1, S16_MIN, s16, true);
+ errors += test_op("sub", cast(s32)S32_MIN, cast(s32)1, S32_MIN, s32, true);
+ errors += test_op("sub", cast(s64)S64_MIN, cast(u32)1, S64_MIN, s64, true);
+
+ errors += test_op("sub", cast( s8)-1, cast( s8) S8_MAX, S8_MIN, s8, false);
+ errors += test_op("sub", cast(s16)-1, cast(s16)S16_MAX, S16_MIN, s16, false);
+ errors += test_op("sub", cast(s32)-1, cast(s32)S32_MAX, S32_MIN, s32, false);
+ errors += test_op("sub", cast(s64)-1, cast(s64)S64_MAX, S64_MIN, s64, false);
+
+ // Test unsigned sub.
+ errors += test_op("sub", cast( u8)1, cast( u8) U8_MAX, 0, u8, true);
+ errors += test_op("sub", cast( u8)1, cast(u16)U16_MAX, 0, u16, true);
+ errors += test_op("sub", cast(u32)1, cast(u32)U32_MAX, 0, u32, true);
+ errors += test_op("sub", cast(u32)1, cast(u64)U64_MAX, 0, u64, true);
+
+ errors += test_op("sub", cast( u8) U8_MAX, cast( u8)0, U8_MAX, u8, false);
+ errors += test_op("sub", cast(u16)U16_MAX, cast( u8)0, U16_MAX, u16, false);
+ errors += test_op("sub", cast(u32)U32_MAX, cast(u32)0, U32_MAX, u32, false);
+ errors += test_op("sub", cast(u64)U64_MAX, cast(u32)0, U64_MAX, u64, false);
+
+ // Test signed mul.
+ errors += test_op("mul", cast( s8) S8_MIN, cast( s8)-1, S8_MAX, s8, true);
+ errors += test_op("mul", cast(s16)S16_MIN, cast( s8)-1, S16_MAX, s16, true);
+ errors += test_op("mul", cast(s32)S32_MIN, cast(s32)-1, S32_MAX, s32, true);
+ errors += test_op("mul", cast(s64)S64_MIN, cast(s32)-1, S64_MAX, s64, true);
+
+ errors += test_op("mul", cast( s8) S8_MAX, cast( s8)-2, S8_MIN, s8, true);
+ errors += test_op("mul", cast(s16)S16_MAX, cast( s8)-2, S16_MIN, s16, true);
+ errors += test_op("mul", cast(s32)S32_MAX, cast(s32)-2, S32_MIN, s32, true);
+ errors += test_op("mul", cast(s64)S64_MAX, cast(s32)-2, S64_MIN, s64, true);
+
+ errors += test_op("mul", cast( s8)-2, cast( s8) S8_MAX, S8_MIN, s8, true);
+ errors += test_op("mul", cast( s8)-2, cast(s16)S16_MAX, S16_MIN, s16, true);
+ errors += test_op("mul", cast(s32)-2, cast(s32)S32_MAX, S32_MIN, s32, true);
+ errors += test_op("mul", cast(s32)-2, cast(s64)S64_MAX, S64_MIN, s64, true);
+
+ errors += test_op("mul", cast( s8) S8_MAX, cast( s8)2, S8_MAX, s8, true);
+ errors += test_op("mul", cast(s16)S16_MAX, cast( s8)2, S16_MAX, s16, true);
+ errors += test_op("mul", cast(s32)S32_MAX, cast(s32)2, S32_MAX, s32, true);
+ errors += test_op("mul", cast(s64)S64_MAX, cast(s32)2, S64_MAX, s64, true);
+
+ errors += test_op("mul", cast( s8) S8_MAX, cast( s8)-1, -S8_MAX, s8, false);
+ errors += test_op("mul", cast(s16)S16_MAX, cast( s8)-1, -S16_MAX, s16, false);
+ errors += test_op("mul", cast(s32)S32_MAX, cast(s32)-1, -S32_MAX, s32, false);
+ errors += test_op("mul", cast(s64)S64_MAX, cast(s32)-1, -S64_MAX, s64, false);
+
+ errors += test_op("mul", cast( s8) S8_MAX, cast( s8)0, 0, s8, false);
+ errors += test_op("mul", cast(s16)S16_MAX, cast( u8)0, 0, s16, false);
+ errors += test_op("mul", cast(s32)S32_MAX, cast(s32)0, 0, s32, false);
+ errors += test_op("mul", cast(s64)S64_MAX, cast(u32)0, 0, s64, false);
+
+ // Test unsigned mul.
+ errors += test_op("mul", cast( u8) U8_MAX, cast( u8)1, U8_MAX, u8, false);
+ errors += test_op("mul", cast(u16)U16_MAX, cast( u8)1, U16_MAX, u16, false);
+ errors += test_op("mul", cast(u32)U32_MAX, cast(u32)1, U32_MAX, u32, false);
+ errors += test_op("mul", cast(u64)U64_MAX, cast(u32)1, U64_MAX, u64, false);
+
+ errors += test_op("mul", cast( u8) U8_MAX, cast( u8)2, U8_MAX, u8, true);
+ errors += test_op("mul", cast(u16)U16_MAX, cast( u8)2, U16_MAX, u16, true);
+ errors += test_op("mul", cast(u32)U32_MAX, cast(u32)2, U32_MAX, u32, true);
+ errors += test_op("mul", cast(u64)U64_MAX, cast(u32)2, U64_MAX, u64, true);
+
+ // Test signed div.
+ errors += test_op("div", cast( s8) S8_MIN, cast( s8)-1, S8_MAX, s8, true, -1);
+ errors += test_op("div", cast(s16)S16_MIN, cast( s8)-1, S16_MAX, s16, true, -1);
+ errors += test_op("div", cast(s32)S32_MIN, cast(s32)-1, S32_MAX, s32, true, -1);
+ errors += test_op("div", cast(s64)S64_MIN, cast(s32)-1, S64_MAX, s64, true, -1);
+
+ errors += test_op("div", cast( s8) S8_MAX, cast( s8)-2, - S8_MAX/2, s8, false, 1);
+ errors += test_op("div", cast(s16)S16_MAX, cast( s8)-2, -S16_MAX/2, s16, false, 1);
+ errors += test_op("div", cast(s32)S32_MAX, cast(s32)-2, -S32_MAX/2, s32, false, 1);
+ errors += test_op("div", cast(s64)S64_MAX, cast(s32)-2, -S64_MAX/2, s64, false, 1);
+
+ errors += test_op("div", cast( s8)15, cast( s8)5, 3, s8, false, 0);
+ errors += test_op("div", cast( u8)15, cast(s16)7, 2, s16, false, 1);
+ errors += test_op("div", cast(s16)15, cast(s32)13, 1, s32, false, 2);
+ errors += test_op("div", cast(u16)100, cast(s64)3, 33, s64, false, 1);
+
+ // Test unsigned div.
+ errors += test_op("div", cast( u8) U8_MAX, cast( u8)2, U8_MAX/2, u8, false, 1);
+ errors += test_op("div", cast(u16)U16_MAX, cast( u8)2, U16_MAX/2, u16, false, 1);
+ errors += test_op("div", cast(u32)U32_MAX, cast(u32)2, U32_MAX/2, u32, false, 1);
+ errors += test_op("div", cast(u64)U64_MAX, cast(u32)2, U64_MAX/2, u64, false, 1);
+
+ if errors > 0 print("# Found % %!\n", errors, ifx errors == 1 then "error" else "errors"); else print(" No errors found.\n");
+
+
+ // Test generic agains branch-free alternative.
+ write_strings(
+ "#=======================#\n",
+ "# generic == x64 asm ? #\n"
+ );
+
+
+ full_test :: ($type: Type, test: (a: type, b: type)) {
+ #if type == {
+ case u8;
+ min :u8 = 0;
+ max :u8 = U8_MAX;
+
+ case u16;
+ min :u16 = 0;
+ max :u16 = U16_MAX;
+
+ case s8;
+ min :s8 = S8_MIN;
+ max :s8 = S8_MAX;
+
+ case s16;
+ min :s16 = S16_MIN;
+ max :s16 = S16_MAX;
+
+ case;
+ assert(false, "This will take way too long.");
+ }
+
+ #if !AVOID_INFINITE_FOR_LOOP {
+ for a : min..max {
+ for b : min..max {
+ test(a, b);
+ }
+ }
+ }
+ else {
+ a :type = min;
+ b :type = min;
+ while loop_a := true {
+ while loop_b := true {
+ test(a, b);
+ if b == max then break loop_b; else b += 1;
+ }
+ if a == max then break loop_a; else a += 1;
+ }
+ }
+ }
+
+ partial_test :: ($type: Type, test: (a: type, b: type)) {
+ min, max: type;
+ #if type == {
+ case u8;
+ min = 0;
+ max = U8_MAX;
+
+ case u16;
+ min = 0;
+ max = U16_MAX;
+
+ case u32;
+ min = 0;
+ max = U32_MAX;
+
+ case s8;
+ min = S8_MIN;
+ max = S8_MAX;
+
+ case s16;
+ min = S16_MIN;
+ max = S16_MAX;
+
+ case s32;
+ min = S32_MIN;
+ max = S32_MAX;
+
+ case;
+ assert(false, "This will take way too long.");
+ }
+
+ #if !AVOID_INFINITE_FOR_LOOP {
+ for a: min..max {
+ b := a;
+ c := max - a + min;
+ test(a, b);
+ test(a, c);
+ }
+ }
+ else {
+ a := min;
+ while loop := true {
+ b := a;
+ c := max - a + min;
+ test(a, b);
+ test(a, c);
+ if a == max then break loop; else a += 1;
+ }
+ }
+ }
+
+ minimal_test :: ($type: Type, test: (a: type, b: type)) {
+ #if type == {
+ case u32;
+ min :u32 = 0;
+ mid :u32 = U32_MAX / 2;
+ max :u32 = U32_MAX;
+ range :u32 = cast(u32)U16_MAX * 2048;
+
+ case u64;
+ min :u64 = 0;
+ mid :u64 = U64_MAX / 2;
+ max :u64 = U64_MAX;
+ range :u64 = cast(u64)U16_MAX * 2048;
+
+ case s32;
+ min :s32 = S32_MIN;
+ mid :s32 = (S32_MIN / 2) + (S32_MAX / 2);
+ max :s32 = S32_MAX;
+ range :s32 = cast(s32)S16_MAX * 2048;
+
+ case s64;
+ min :s64 = S64_MIN;
+ mid :s64 = (S64_MIN / 2) + (S64_MAX / 2);
+ max :s64 = S64_MAX;
+ range :s64 = cast(s64)S16_MAX * 2048;
+
+ case;
+ assert(false, "Invalid type % given.", type);
+ }
+
+ #if !AVOID_INFINITE_FOR_LOOP {
+ start, end : type;
+
+ start = min;
+ end = min+range;
+ for a: start..end {
+ b := a;
+ c := end - a + start;
+ test(a, b);
+ test(a, c);
+ }
+
+ start = mid-range;
+ end = mid+range;
+ for a: start..end {
+ b := a;
+ c := end - a + start;
+ test(a, b);
+ test(a, c);
+ }
+
+ start = max-range;
+ end = max;
+ for a: start..end {
+ b := a;
+ c := end - a + start;
+ test(a, b);
+ test(a, c);
+ }
+ }
+ else {
+ start, end, a : type;
+
+ start = min;
+ end = min + range;
+ a = start;
+ while loop := true {
+ b := a;
+ c := end - a + start;
+ test(a, b);
+ test(a, c);
+ if a == end then break loop; else a += 1;
+ }
+
+ start = mid - range;
+ end = mid + range;
+ a = start;
+ while loop := true {
+ b := a;
+ c := end - a + start;
+ test(a, b);
+ test(a, c);
+ if a == end then break loop; else a += 1;
+ }
+
+ start = max - range;
+ end = max;
+ a = start;
+ while loop := true {
+ b := a;
+ c := end - a + start;
+ test(a, b);
+ test(a, c);
+ if a == end then break loop; else a += 1;
+ }
+ }
+ }
+
+
+ // add
+
+ ADD_TEST_TEMPLATE :: (a: $T, b: T) {
+ rT, sT := add(a, b, true);
+ rF, sF := add(a, b, false);
+ assert(rT == rF && sT == sF, "> add(%1, %2, true) = %3,%4 != add(%1, %2, false) = %5,%6\n", a, b, rT, sT, rF, sF);
+ }
+
+ write_string("# testing add,u8 #\n");
+ full_test(u8, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,u16 #\n");
+ full_test(u16, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,u32 #\n");
+ partial_test(u32, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,u64 #\n");
+ minimal_test(u64, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,s8 #\n");
+ full_test(s8, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,s16 #\n");
+ full_test(s16, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,s32 #\n");
+ partial_test(s32, ADD_TEST_TEMPLATE);
+
+ write_string("# testing add,s64 #\n");
+ minimal_test(s64, ADD_TEST_TEMPLATE);
+
+
+ // sub
+
+ SUB_TEST_TEMPLATE :: (a: $T, b: T) {
+ rT, sT := sub(a, b, true);
+ rF, sF := sub(a, b, false);
+ assert(rT == rF && sT == sF, "> sub(%1, %2, true) = %3,%4 != sub(%1, %2, false) = %5,%6\n", a, b, rT, sT, rF, sF);
+ }
+
+ write_string("# testing sub,u8 #\n");
+ full_test(u8, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,u16 #\n");
+ full_test(u16, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,u32 #\n");
+ partial_test(u32, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,u64 #\n");
+ minimal_test(u64, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,s8 #\n");
+ full_test(s8, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,s16 #\n");
+ full_test(s16, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,s32 #\n");
+ partial_test(s32, SUB_TEST_TEMPLATE);
+
+ write_string("# testing sub,s64 #\n");
+ minimal_test(s64, SUB_TEST_TEMPLATE);
+
+
+ // mul
+
+ MUL_TEST_TEMPLATE :: (a: $T, b: T) {
+ rT, sT := mul(a, b, true);
+ rF, sF := mul(a, b, false);
+ assert(rT == rF && sT == sF, "> mul(%1, %2, true) = %3,%4 != mul(%1, %2, false) = %5,%6\n", a, b, rT, sT, rF, sF);
+ }
+
+ write_string("# testing mul,u8 #\n");
+ full_test(u8, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,u16 #\n");
+ full_test(u16, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,u32 #\n");
+ partial_test(u32, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,u64 #\n");
+ minimal_test(u64, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,s8 #\n");
+ full_test(s8, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,s16 #\n");
+ full_test(s16, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,s32 #\n");
+ partial_test(s32, MUL_TEST_TEMPLATE);
+
+ write_string("# testing mul,s64 #\n");
+ minimal_test(s64, MUL_TEST_TEMPLATE);
+
+
+ // div
+
+ DIV_TEST_TEMPLATE :: (a: $T, b: T) {
+ if b == 0 then return;
+ rT, remT, sT := div(a, b, true);
+ rF, remF, sF := div(a, b, false);
+ assert(rT == rF && sT == sF, "> mul(%1, %2, true) = %3,%4,%5 != mul(%1, %2, false) = %6,%7,%8\n", a, b, rT, remT, sT, rF, remF, sF);
+ }
+
+ write_string("# testing div,u8 #\n");
+ full_test(u8, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,u16 #\n");
+ full_test(u16, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,u32 #\n");
+ partial_test(u32, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,u64 #\n");
+ minimal_test(u64, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,s8 #\n");
+ full_test(s8, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,s16 #\n");
+ full_test(s16, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,s32 #\n");
+ partial_test(s32, DIV_TEST_TEMPLATE);
+
+ write_string("# testing div,s64 #\n");
+ minimal_test(s64, DIV_TEST_TEMPLATE);
+
+
+ write_string(" No errors found.\n");
+
+
+ write_strings(
+ "#=======================#\n",
+ "# Benchmarks #\n"
+ );
+
+ #import "Random";
+
+ performance_test :: ($operation: string, $type: Type, print_result: bool = true) -> ops_per_us_gen: float, ops_per_us_asm: float {
+
+ #if type == u8 { MIN :: 0; MAX :: U8_MAX; }
+ #if type == u16 { MIN :: 0; MAX :: U16_MAX; }
+ #if type == u32 { MIN :: 0; MAX :: U32_MAX; }
+ #if type == u64 { MIN :: 0; MAX :: U64_MAX; }
+ #if type == s8 { MIN :: S8_MIN; MAX :: S8_MAX; }
+ #if type == s16 { MIN :: S16_MIN; MAX :: S16_MAX; }
+ #if type == s32 { MIN :: S32_MIN; MAX :: S32_MAX; }
+ #if type == s64 { MIN :: S64_MIN; MAX :: S64_MAX; }
+
+ NUM_TESTS :: 50000;
+ DATA_SIZE_BITS :: 64*1024*8;
+ #if type == s8 || type == u8 then
+ DATA_SIZE :: DATA_SIZE_BITS/8;
+ else #if type == s16 || type == u16 then
+ DATA_SIZE :: DATA_SIZE_BITS/16;
+ else #if type == s32 || type == u32 then
+ DATA_SIZE :: DATA_SIZE_BITS/32;
+ else #if type == s64 || type == u64 then
+ DATA_SIZE :: DATA_SIZE_BITS/64;
+
+ best_gen := 0.0;
+ best_asm := 0.0;
+ numbers_x: [..] type;
+ numbers_y: [..] type;
+ array_reserve(*numbers_x, DATA_SIZE);
+ array_reserve(*numbers_y, DATA_SIZE);
+
+ // Comment the line bellow to use the same "random" values.
+ random_seed(cast(u64)to_nanoseconds(current_time_monotonic()));
+
+ for 0..DATA_SIZE-1 {
+ x := cast(type) random_get_within_range(xx MIN, xx MAX);
+ y := cast(type) random_get_within_range(xx MIN, xx MAX);
+ if y == 0 && operation == "div" {
+ y = 1;
+ }
+ array_add(*numbers_x, x);
+ array_add(*numbers_y, y);
+ }
+
+ for 0..NUM_TESTS-1 {
+
+ r_gen: type = 0;
+ r_asm: type = 0;
+
+ time_gen := current_time_monotonic();
+ for idx: 0..DATA_SIZE-1 #insert #run replace("r_gen ^= OP(numbers_x[idx], numbers_y[idx], false);", "OP", operation);
+ time_gen = current_time_monotonic() - time_gen;
+
+ time_asm := current_time_monotonic();
+ for idx: 0..DATA_SIZE-1 #insert #run replace("r_asm ^= OP(numbers_x[idx], numbers_y[idx], true);", "OP", operation);
+ time_asm = current_time_monotonic() - time_asm;
+
+ assert(r_gen == r_asm);
+
+ perf_gen := cast(float)DATA_SIZE/cast(float)to_nanoseconds(time_gen);
+ perf_asm := cast(float)DATA_SIZE/cast(float)to_nanoseconds(time_asm);
+ best_gen = max(best_gen, perf_gen);
+ best_asm = max(best_asm, perf_asm);
+ }
+
+ tmp_context := context;
+ push_context tmp_context {
+ ff := *context.print_style.default_format_float;
+ ff.zero_removal = .NO;
+ ff.width = 7;
+ ff.trailing_width = 2;
+
+ fi := *context.print_style.default_format_int;
+ fi.minimum_digits = 3;
+
+ if print_result {
+ if type == s8 || type == u8 write_string(" ");
+ print("% | % | % | %\n", type, best_gen, best_asm, cast(int)(100*best_asm/best_gen));
+ }
+ }
+ return best_gen, best_asm;
+ }
+
+ write_strings(
+ " | (ops / nsec) |\n",
+ " T | generic | x64 asm | %\n"
+ );
+
+ write_strings(
+ "--- | ----------------- | ---\n",
+ " | add |\n"
+ );
+ performance_test("add", u8);
+ performance_test("add", u16);
+ performance_test("add", u32);
+ performance_test("add", u64);
+ performance_test("add", s8);
+ performance_test("add", s16);
+ performance_test("add", s32);
+ performance_test("add", s64);
+
+ write_strings(
+ "--- | ----------------- | ---\n",
+ " | sub |\n"
+ );
+ performance_test("sub", u8);
+ performance_test("sub", u16);
+ performance_test("sub", u32);
+ performance_test("sub", u64);
+ performance_test("sub", s8);
+ performance_test("sub", s16);
+ performance_test("sub", s32);
+ performance_test("sub", s64);
+
+ write_strings(
+ "--- | ----------------- | ---\n",
+ " | mul |\n"
+ );
+ performance_test("mul", u8);
+ performance_test("mul", u16);
+ performance_test("mul", u32);
+ performance_test("mul", u64);
+ performance_test("mul", s8);
+ performance_test("mul", s16);
+ performance_test("mul", s32);
+ performance_test("mul", s64);
+
+ write_strings(
+ "--- | ----------------- | ---\n",
+ " | div |\n"
+ );
+ performance_test("div", u8);
+ performance_test("div", u16);
+ performance_test("div", u32);
+ performance_test("div", u64);
+ performance_test("div", s8);
+ performance_test("div", s16);
+ performance_test("div", s32);
+ performance_test("div", s64);
+}
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;
+}
diff --git a/UTF8/module.jai b/UTF8/module.jai
new file mode 100644
index 0000000..5e6fd65
--- /dev/null
+++ b/UTF8/module.jai
@@ -0,0 +1,149 @@
+// Some procedures to help working with UTF8 strings.
+// https://en.wikipedia.org/wiki/UTF-8
+
+// Returns true if argument is a continuation byte.
+is_continuation_byte :: inline (byte: u8) -> bool {
+ // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte
+ return (byte & 0xC0) == 0x80;
+}
+
+// Given a leading_byte, returns the number of bytes on the character.
+count_character_bytes :: inline (character_leading_byte: u8) -> int {
+ // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte
+ if (character_leading_byte & 0xE0) == 0xC0 return 1+1;
+
+ // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte
+ if (character_leading_byte & 0xF0) == 0xE0 return 1+2;
+
+ // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte
+ if (character_leading_byte & 0xF8) == 0xF0 return 1+3;
+
+ return 1;
+}
+
+// Returns the string (using same str.data) truncated to the provided length.
+truncate :: (str: string, length: int) -> string {
+
+ if str.data == null then return "";
+
+ if str.count < length then return .{str.count, str.data};
+
+ data := str.data;
+ count := str.count;
+
+ // Find index of first continuation byte.
+ idx := length;
+ while (idx > 0 && is_continuation_byte(data[idx - 1])) {
+ idx -= 1;
+ }
+ continuation_bytes := length - idx;
+
+ // If string starts with continuation bytes, it's an invalid UTF8 string.
+ if (idx == 0 && continuation_bytes > 0) {
+ length = 0;
+ }
+ // If length truncates some continuation bytes, remove incomplete UTF8 character.
+ else if (idx > 0 // string is not empty
+ // continuation bytes are not complete
+ && !(continuation_bytes == 0 && (data[idx - 1] & 0x80) == 0x00)
+ && !(continuation_bytes == 1 && (data[idx - 1] & 0xE0) == 0xC0)
+ && !(continuation_bytes == 2 && (data[idx - 1] & 0xF0) == 0xE0)
+ && !(continuation_bytes == 3 && (data[idx - 1] & 0xF8) == 0xF0)
+ ) {
+ length -= (continuation_bytes + 1); // Remove start byte, hence '+ 1'.
+ }
+
+ return .{length, str.data};
+}
+
+// Returns true when the string is empty or consists of space characters.
+is_empty :: (str: string) -> bool {
+ for 0..str.count-1 {
+ if str[it] == {
+ case #char "\0"; #through;
+ case #char "\t"; #through; // horizontal tab
+ case #char "\n"; #through; // line feed
+ case #char "\x0B"; #through; // vertical tabulation
+ case #char "\x0C"; #through; // form feed
+ case #char "\r"; #through; // carriage return
+ case #char " ";
+ continue;
+ case;
+ return false;
+ }
+ }
+ return true;
+}
+
+// Counts the number of characters.
+count_characters :: (str: string, $is_null_terminated := false) -> int {
+ characters := 0;
+ idx := 0;
+
+ #if is_null_terminated {
+ while idx < str.count && str[idx] != 0 {
+ idx += count_character_bytes(str[idx]);
+ characters += 1;
+ }
+ }
+ else {
+ while idx < str.count {
+ idx += count_character_bytes(str[idx]);
+ characters += 1;
+ }
+ }
+
+ return characters;
+}
+
+// Deletes character by it's index, and moves tail data to take its place.
+delete_character :: (str: *string, character_idx: int) -> success := true {
+ buffer_idx := get_byte_index(str.*, character_idx);
+
+ if buffer_idx < 0 return false;
+
+ bytes_to_delete := count_character_bytes(str.data[buffer_idx]);
+
+ for buffer_idx..str.count-1-bytes_to_delete {
+ str.data[it] = str.data[it+bytes_to_delete];
+ }
+ for str.count-bytes_to_delete..str.count-1 {
+ str.data[it] = 0;
+ }
+
+ str.count -= bytes_to_delete;
+ return;
+}
+
+// Searches for the given character index and returns its byte index on the string.
+get_byte_index :: (str: string, character_index: int) -> buffer_index: int, success := true {
+ buff_idx := 0;
+ char_idx := 0;
+ while buff_idx < str.count {
+ if char_idx == character_index return buff_idx;
+ buff_idx += count_character_bytes(str[buff_idx]);
+ char_idx += 1;
+ }
+ return -1, false;
+}
+
+// Scans the string for UTF8 encoding errors.
+is_valid :: (str: string) -> is_valid := true, error_index: int = -1 {
+ idx := 0;
+ remainig_bytes := 0;
+ while idx < str.count {
+ defer idx += 1;
+
+ is_continuation := is_continuation_byte(str[idx]);
+
+ if (is_continuation && remainig_bytes == 0) || (!is_continuation && remainig_bytes > 0) then return false, idx;
+
+ if is_continuation {
+ remainig_bytes -= 1;
+ continue;
+ }
+
+ remainig_bytes = count_character_bytes(str[idx]) - 1;
+ }
+ return;
+}
diff --git a/UTF8/tests.jai b/UTF8/tests.jai
new file mode 100644
index 0000000..b7e3579
--- /dev/null
+++ b/UTF8/tests.jai
@@ -0,0 +1,162 @@
+#import "Basic";
+#import "String";
+#import "UTF8";
+
+main :: () {
+ write_strings(
+ "#=======================#\n",
+ "# Basic tests #\n"
+ );
+
+ tmp_str: string;
+ tmp_bool: bool;
+ tmp_int: int;
+
+ assert(is_continuation_byte("0€1"[0]) == false);
+ assert(is_continuation_byte("0€1"[1]) == false);
+ assert(is_continuation_byte("0€1"[2]) == true);
+ assert(is_continuation_byte("0€1"[3]) == true);
+ assert(is_continuation_byte("0€1"[4]) == false);
+
+
+ write_strings("# count_character_bytes #\n");
+
+ assert(count_character_bytes("0£€𐍈1"[0]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[1]) == 2);
+ assert(count_character_bytes("0£€𐍈1"[2]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[3]) == 3);
+ assert(count_character_bytes("0£€𐍈1"[4]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[5]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[6]) == 4);
+ assert(count_character_bytes("0£€𐍈1"[7]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[8]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[9]) == 1);
+ assert(count_character_bytes("0£€𐍈1"[10]) == 1);
+
+
+ write_strings("# truncate #\n");
+
+ assert(compare(truncate("0£€𐍈1", 0), "") == 0);
+ assert(compare(truncate("0£€𐍈1", 1), "0") == 0);
+ assert(compare(truncate("0£€𐍈1", 2), "0") == 0);
+ assert(compare(truncate("0£€𐍈1", 3), "0£") == 0);
+ assert(compare(truncate("0£€𐍈1", 4), "0£") == 0);
+ assert(compare(truncate("0£€𐍈1", 5), "0£") == 0);
+ assert(compare(truncate("0£€𐍈1", 6), "0£€") == 0);
+ assert(compare(truncate("0£€𐍈1", 7), "0£€") == 0);
+ assert(compare(truncate("0£€𐍈1", 8), "0£€") == 0);
+ assert(compare(truncate("0£€𐍈1", 9), "0£€") == 0);
+ assert(compare(truncate("0£€𐍈1", 10), "0£€𐍈") == 0);
+ assert(compare(truncate("0£€𐍈1", 11), "0£€𐍈1") == 0);
+ assert(compare(truncate("0£€𐍈1", 12), "0£€𐍈1") == 0);
+
+
+ write_strings("# is_empty #\n");
+
+ assert(is_empty(""));
+ assert(is_empty("\0"));
+ assert(is_empty("\0\t"));
+ assert(is_empty("\0\t\n"));
+ assert(is_empty("\0\t\n\x0B"));
+ assert(is_empty("\0\t\n\x0B\x0C"));
+ assert(is_empty("\0\t\n\x0B\x0C\r"));
+ assert(is_empty("\0\t\n\x0B\x0C\r "));
+ assert(is_empty("\0\t\n\x0B\x0C\r .") == false);
+ assert(is_empty("| B A Z € N G A |") == false);
+
+
+ write_strings("# delete_character #\n");
+
+ tmp_str = copy_string("",, temporary_allocator);
+ assert(delete_character(*tmp_str, 0) == false);
+
+ tmp_str = copy_string("12£45€78𐍈",, temporary_allocator);
+ assert(delete_character(*tmp_str, -1) == false);
+ assert(delete_character(*tmp_str, 99999) == false);
+ assert(delete_character(*tmp_str, 7) == true);
+ assert(compare(tmp_str, "12£45€7𐍈") == 0);
+ assert(delete_character(*tmp_str, 2) == true);
+ assert(compare(tmp_str, "1245€7𐍈") == 0);
+ assert(delete_character(*tmp_str, 4) == true);
+ assert(compare(tmp_str, "12457𐍈") == 0);
+ assert(delete_character(*tmp_str, 3) == true);
+ assert(compare(tmp_str, "1247𐍈") == 0);
+
+
+ write_strings("# get_byte_index #\n");
+
+ tmp_str = copy_string("12£45€78𐍈X",, temporary_allocator);
+
+ tmp_int, tmp_bool = get_byte_index("", 0);
+ assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, -1);
+ assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, -99999);
+ assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 99999);
+ assert(tmp_int == -1 && tmp_bool == false, "(%, %)", tmp_int, tmp_bool);
+
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 0);
+ assert(tmp_int == 0 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 1);
+ assert(tmp_int == 1 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 2);
+ assert(tmp_int == 2 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 3);
+ assert(tmp_int == 4 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 4);
+ assert(tmp_int == 5 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 5);
+ assert(tmp_int == 6 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 6);
+ assert(tmp_int == 9 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 7);
+ assert(tmp_int == 10 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 8);
+ assert(tmp_int == 11 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+ tmp_int, tmp_bool = get_byte_index(tmp_str, 9);
+ assert(tmp_int == 15 && tmp_bool == true, "(%, %)", tmp_int, tmp_bool);
+
+
+ write_strings("# count_characters #\n");
+
+ assert(count_characters("") == 0);
+ assert(count_characters("0") == 1);
+ assert(count_characters("0£") == 2);
+ assert(count_characters("0£€") == 3);
+ assert(count_characters("0£€𐍈") == 4);
+ assert(count_characters("0£€𐍈1") == 5);
+
+ tmp_str = copy_string("123€DELETE",, temporary_allocator);
+ tmp_str[6] = 0;
+ assert(count_characters(tmp_str) == 10);
+ assert(count_characters(tmp_str, true) == 4);
+
+
+ write_strings("# is_valid #\n");
+
+ assert(is_valid(""));
+
+ tmp_str = copy_string("123€DELETE",, temporary_allocator);
+ tmp_str[6] = 0;
+ tmp_bool, tmp_int = is_valid(tmp_str);
+ assert(tmp_bool == true && tmp_int == -1, "(%, %)", tmp_bool, tmp_int);
+
+ tmp_str = copy_string("123€DELETE",, temporary_allocator);
+ tmp_str[3] = 0; // Cut € at start.
+ tmp_bool, tmp_int = is_valid(tmp_str);
+ assert(tmp_bool == false && tmp_int == 4, "(%, %)", tmp_bool, tmp_int);
+
+ tmp_str = copy_string("123€DELETE",, temporary_allocator);
+ tmp_str[4] = 0; // Cut € at middle.
+ tmp_bool, tmp_int = is_valid(tmp_str);
+ assert(tmp_bool == false && tmp_int == 4, "(%, %)", tmp_bool, tmp_int);
+
+ tmp_str = copy_string("123€DELETE",, temporary_allocator);
+ tmp_str[5] = 0; // Cut € at end.
+ tmp_bool, tmp_int = is_valid(tmp_str);
+ assert(tmp_bool == false && tmp_int == 5, "(%, %)", tmp_bool, tmp_int);
+
+
+ write_strings(" No errors found.\n");
+}