aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordam <dam@gudinoff>2024-05-29 12:50:48 +0100
committerdam <dam@gudinoff>2024-05-29 12:50:48 +0100
commit986c0ca11d45e83e97479fcfad5facd1e56b0beb (patch)
tree8576d455c6748a38e81787b308fb8cbbe1ad7b89
parent393e5a926cd105c4a2f902824a233cc41af91198 (diff)
parentec706533ca26d49670adb97617df0d565528e395 (diff)
downloadtask-time-tracker-2.0.tar.zst
task-time-tracker-2.0.zip
Merge with jai-prototypev2.0
-rw-r--r--.hgignore1
-rw-r--r--LICENSE-GPL-3.0-or-later (renamed from COPYING)0
-rw-r--r--README.md128
-rw-r--r--modules/LICENSE-ISC15
-rw-r--r--modules/LICENSE-MIT21
-rw-r--r--modules/README.md34
-rw-r--r--modules/Saturation/module.jai418
-rw-r--r--modules/Saturation/tests.jai647
-rw-r--r--modules/TUI/examples/snake.jai188
-rw-r--r--modules/TUI/key_map.jai505
-rw-r--r--modules/TUI/module.jai805
-rw-r--r--modules/TUI/palette_24b.jai50
-rw-r--r--modules/TUI/palette_4b.jai19
-rw-r--r--modules/TUI/palette_8b.jai307
-rw-r--r--modules/TUI/tests.jai232
-rw-r--r--modules/TUI/unix.jai319
-rw-r--r--modules/TUI/windows.jai390
-rw-r--r--modules/UTF8/module.jai149
-rw-r--r--modules/UTF8/tests.jai162
-rw-r--r--readme.md77
-rw-r--r--ttt.jai1806
-rw-r--r--unused.c3
-rw-r--r--unused.jai353
23 files changed, 6550 insertions, 79 deletions
diff --git a/.hgignore b/.hgignore
new file mode 100644
index 0000000..30bcfa4
--- /dev/null
+++ b/.hgignore
@@ -0,0 +1 @@
+.build/
diff --git a/COPYING b/LICENSE-GPL-3.0-or-later
index f288702..f288702 100644
--- a/COPYING
+++ b/LICENSE-GPL-3.0-or-later
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6167659
--- /dev/null
+++ b/README.md
@@ -0,0 +1,128 @@
+Task Time Tracker
+=================
+
+A tool to keep track of the time spent on tasks.
+
+# Why use it?
+
+Why not? Besides, you'll be able to create, duplicate, move, rename,
+archive, restore and delete tasks at the distance of one (or two) key
+stroke(s). You'll also be able to edit the time spent on a task by
+adding, subtracting or setting the time you want. But that's not all!
+
+With the Track'n'Close technology, you won't need to keep the app always
+open. Once you start tracking time for a task, the app will keep track
+of it, even if you close it or turn off your computer.
+
+Count seconds, minutes, hours, days, and even years... all the way up to
+infinity\* thanks to the not-so-new technology of 64 bit integers.
+
+Be amazed by the compact interface that automatically adapts the time
+representations while maximizing the displayed precision.
+
+Cleanup the workspace by moving your finished tasks to the archive. Want
+to bring back some archived tasks? No problem, just switch into the
+archive view and restore them.
+
+Ever felt like your data is being held hostage? Not anymore! Import and
+export your tasks using the widely supported CSV text file format.
+
+Want to be part of the last frontier? Grab the bleeding edge version 2
+which brings: sorting capabilities; better text input with UTF8
+support; possibility to archive and reset all current tasks with a
+single command; and allows to merge tasks with the same name. As an
+extra , you'll need to export and re-import your tasks due to some
+database incompatibility (oh, the joy).
+
+And if you don't like what you're seeing, this is your lucky day!
+Because you have access to the source code, you can adapt it to your
+needs!
+
+**Task Time Tracker - *You may not need it, but I enjoyed making it!***
+
+*\* Although the app cannot count to infinity, it surely displays it (∞)
+if the time goes to 9999.5 years or above. Also, it will display a minus
+(-) if you force a time to have negative values.*
+
+# Why create such tool?
+
+I like to keep track of the time spent on my daily job's tasks. It helps
+me be aware of time pits, and improve my time estimates. This motivated
+me to search for a simple app that allowed to take measurements without
+too much effort.
+
+After skimming through all cloud-based and too-complex apps, I
+eventually landed on some text-based user interface (TUI) apps. The one
+that almost convinced me was [worklog](https://github.com/atsb/worklog),
+but I just couldn't come to terms with its interface.
+
+So, after spending more time than I'd like to admit searching for a task
+time tracker, I decided to take matters on my own hands. Maybe I just
+needed to improve my search-fu. Maybe it was just an excuse to write
+some code. Either way, I wasn't turning back.
+
+# Why use C and ncurses?
+
+I've been looking for an excuse to revisit C, which I haven't used for
+more than a decade, and this seemed like a good opportunity: a simple
+app with a small set of features. But how would I build the user
+interface? I didn't want to learn an advanced toolkit just for this
+small project, and since the TUI apps had somehow resonated with me, I
+decided to try out the ncurses library which has been surviving the test
+of time.
+
+Because this was an hobby project focused in exploring C and its
+standard library, I allowed myself to obsess with whatever details I
+wanted to. This serves to justify the lousy code, and explains the fun I
+had.
+
+Overall, it was a satisfying experience with occasional moments of
+frustration whenever "string" manipulations were required. And now that
+the first part is completed the best part begins: let's try to implement
+it in Jai and get some hands-on experience of how the two compare. See
+you on the other side. 🖖
+
+# What is Jai?
+
+Jai is a temporary name used for the programming language being
+developed at Thekla, Inc.
+
+# Why port it to Jai?
+
+Because I love to explore, and I needed an excuse to try out the Jai
+compiler I got access in the meantime. This allowed me to experiment and
+compare this new programing language against the original C
+implementation.
+
+During the initial pass to port the code, I made some small adaptations
+due to syntax differences, and improved some data types (finally, I
+could replace all those `*u8` with proper `string`). The initial pass
+was easy, so I decided to add some features I missed while using the app
+on my daily job.
+
+Still, the ncurses dependency was bothering me. Although the language is
+well prepared to interact with C/C++ libraries, and I was able to
+quickly setup the necessary bindings to use ncurses, it didn’t fell
+right… this dependency was blocking me from building this app for
+different operating systems (OS). Could I replace ncurses with something
+native to this language? Yes… but that require much reading and coding,
+and that’s what I did.
+
+After surfing an uncountable number of websites and manuals about linux,
+terminals, escape codes, and whatnot, I ended up creating
+[TUI](https://github.com/gudinoff/jai-modules), a simple terminal user
+interface module that provides basic functionalities similar to the
+[ncurses library](https://en.wikipedia.org/wiki/Ncurses), written in Jai
+to allow portability between OSs.
+
+Working with this new language was a joyful experience. Most of the time
+it felt like I was simply cleaning up (simplifying) the code, with very
+little friction. There was no hiccups setting up the project or adding
+new modules, it all just worked. I really hope this language gets to
+spread its wings.
+
+# License
+
+Licensed under GPL-3.0-or-later.
+
+SPDX-License-Identifier: GPL-3.0-or-later
diff --git a/modules/LICENSE-ISC b/modules/LICENSE-ISC
new file mode 100644
index 0000000..3ca0ef1
--- /dev/null
+++ b/modules/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/modules/LICENSE-MIT b/modules/LICENSE-MIT
new file mode 100644
index 0000000..1632077
--- /dev/null
+++ b/modules/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/modules/README.md b/modules/README.md
new file mode 100644
index 0000000..d9b5839
--- /dev/null
+++ b/modules/README.md
@@ -0,0 +1,34 @@
+jai-modules
+===========
+
+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/modules/Saturation/module.jai b/modules/Saturation/module.jai
new file mode 100644
index 0000000..50c9b3c
--- /dev/null
+++ b/modules/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/modules/Saturation/tests.jai b/modules/Saturation/tests.jai
new file mode 100644
index 0000000..2a82300
--- /dev/null
+++ b/modules/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/modules/TUI/examples/snake.jai b/modules/TUI/examples/snake.jai
new file mode 100644
index 0000000..b62136c
--- /dev/null
+++ b/modules/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/modules/TUI/key_map.jai b/modules/TUI/key_map.jai
new file mode 100644
index 0000000..f0754a7
--- /dev/null
+++ b/modules/TUI/key_map.jai
@@ -0,0 +1,505 @@
+#import "Hash_Table";
+
+key_map: Table(string, Key);
+
+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/modules/TUI/module.jai b/modules/TUI/module.jai
new file mode 100644
index 0000000..124b906
--- /dev/null
+++ b/modules/TUI/module.jai
@@ -0,0 +1,805 @@
+/*
+ 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
+ );
+
+ 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();
+
+ rows, columns: int = ---;
+
+ flush_input();
+ write_string(Commands.QueryWindowSizeInChars);
+
+ // Wait a bit for a response to QueryWindowSizeInChars...
+ 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, assuming that the response is at the end of the input (because we used a terminator character during read).
+ start_idx := find_index_from_right(input, #char "\e");
+ if start_idx > 0 then advance(*input, start_idx);
+
+ 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, assuming that the response is at the end of the input (because we used a terminator character during read).
+ start_idx := find_index_from_right(input, #char "\e");
+ if start_idx > 0 then advance(*input, start_idx);
+
+ 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/modules/TUI/palette_24b.jai b/modules/TUI/palette_24b.jai
new file mode 100644
index 0000000..7545a0b
--- /dev/null
+++ b/modules/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/modules/TUI/palette_4b.jai b/modules/TUI/palette_4b.jai
new file mode 100644
index 0000000..b0317d2
--- /dev/null
+++ b/modules/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/modules/TUI/palette_8b.jai b/modules/TUI/palette_8b.jai
new file mode 100644
index 0000000..36a512f
--- /dev/null
+++ b/modules/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/modules/TUI/tests.jai b/modules/TUI/tests.jai
new file mode 100644
index 0000000..a740e6b
--- /dev/null
+++ b/modules/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/modules/TUI/unix.jai b/modules/TUI/unix.jai
new file mode 100644
index 0000000..99cc61d
--- /dev/null
+++ b/modules/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/modules/TUI/windows.jai b/modules/TUI/windows.jai
new file mode 100644
index 0000000..f8d8bc8
--- /dev/null
+++ b/modules/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/modules/UTF8/module.jai b/modules/UTF8/module.jai
new file mode 100644
index 0000000..5e6fd65
--- /dev/null
+++ b/modules/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/modules/UTF8/tests.jai b/modules/UTF8/tests.jai
new file mode 100644
index 0000000..b7e3579
--- /dev/null
+++ b/modules/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");
+}
diff --git a/readme.md b/readme.md
deleted file mode 100644
index c54ecd7..0000000
--- a/readme.md
+++ /dev/null
@@ -1,77 +0,0 @@
-Task Time Tracker
-=================
-
-# know-how
-- [ncurses colors](https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/color.html#COLORBASICS)
-- [pprintf](https://cplusplus.com/reference/cstdio/printf/)
-- [intmax_t](https://wiki.sei.cmu.edu/confluence/plugins/servlet/mobile?contentId=87152366#content/view/87152366)
-- [hexed.it](https://hexed.it/)
-
-# to-do list
-- [x] Include check on number of char bits;
-- [x] Decide once for all if I'll be using uint8_t or char for strings: use char.
-- [x] maybe rename to task-time-tracker?
-- [x] Remove hash stuff;
-- [x] Tasks should have a `modified_on` timestamp field;
-- [x] Change capacity to size_t.
-- [x] Change active_task to active_task_ptrdiff.
-- [x] Use selected_task_ptrdiff?
-- [x] Make sure task names don't include commas ',';
-- [x] Format time being displayed.
-- [x] Replace max_capacity by its true value;
-- [x] Replace intmax_t by int64_t;
-- [x] Adapt input cycle to work with `database_t *db` to allow pointing to database/archive.
-- [x] Show a symbol to let the user know when we're seeing the archive.
-- [x] Status of task will allow to keep counting time even when the process gets terminated forcefully;
-- [x] Review code: char !uint8_t;
-- [x] Make sure that only one task is running at each time;
-- [x] Mouse selection is broken due to entire TUI update: No, it was fixed by using `erase()` on the `draw_tui` instead of `clear()`;
-- [x] I bet the headers are no longer being used all on a single cycle. Let's separate them and include "header_title_archive";
-- [x] Rename layout members: title_header, archive_header, total_header, days_headers, column_widths, column_alignments, headers_paddings.
-- [x] Using the archive header, we can remove the top-left-corner diamond on the archive.
-- [x] Allow to cancel a rename_task operation: you can do it by leaving it blank.
-- [x] Make sure we are not using `strcat` and `strcpy`... or that we are using them wisely (famous last words).
-- [x] Make archive be stored in CSV format: takes less space and allows to quickly archive by appending to end of file;
-- [x] Implement `append_to_csv(task_t *task, char *path_name)` and use it in archive function;
-- [x] At startup, check for required files and create them if not present.
-- [x] Allow to archive task using keys: `a` and `A`;
-- [x] By default, store files on `~/.config/task_time_tracker/` or `~/.local/share/task_time_tracker` and allow to store elsewhere if passed by argument `--config`.
-- [x] Allow usage of `ttt: ./ttt --dpath ./` to change the app folder: To changes app data path change the environment variable HOME (USERPROFILE for windows users).
-- [x] Clone (replicate) task; If task is active, mark newly created task as inactive;
-- [x] Check if next/previous is safe against overflows/underflows using https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html
-- [x] Confirm delete_task operation by show confirmation message on selected line (horizontally centered).
-- [x] Check totals update speedup using https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html
- - For 1M entries, generic C code runs in 12.0ms while special approaches using builtins or SIMD takes around 9.5ms.
- - Used optimization described [here](https://stackoverflow.com/questions/17580118/signed-saturated-add-of-64-bit-ints).
-- [x] Allow to jump to specific task by index number using key `g` and `G`;
-- [x] Move task to (using task_t tmp_task + memcpy) using key `m` and `M`;
-- [x] Rethink keys;
- - [x] Create task using keys: `n` and `N`;
- - [x] Delete task using key: delete;
- - [x] Change task name using keys: `F2`;
- - [x] Duplicate task using keys: `d` and `D`;
-- [x] Add/remove time using keys: `F3`;
-- [x] Add/remove time for any day of week;
-- [x] Total times may saturate, but before that the user will see the infinite symbol. Solution: Provide user with possibility to refresh totals.
-- [x] Decide on a INVALID_WINDOW_MESSAGE.
-- [x] Use backspace to clear all timers for current task.
-- [x] Move `store_database_partial` to misc and save only when leaving or after 15 seconds of inactivity and having dirty flag set.
-- [x] Register kill signals to exit gracefully.
-- [x] Check if string_buffer needs to be cleared. We may be leaking info on the string_buffer.
-- [x] Replaced `sprintf` by `snprintf`;
-- [x] Make sure that string_buffer bounds are respected;
-- [x] Rename `MAX_TASK_NAME` to something more informative;
-- [x] Compress code:
- - [x] Re-do sprint_time5_utf8: -12 delta LOC;
- - [x] Re-do truncate_string_utf8: 0 delta LOC;
- - [x] Implement `read_input_to_string_buffer`: -24 delta LOC;
- - [x] Wrap malloc (and maybe others) in a function with error checking;
- - [x] Move database actions into functions;
-- [x] Fix bug: archiving/unarchiving task introduces " ," at end of name and increases the number of spaces before comma;
-- [x] Check if draw_tui may be simplified by drawing entire lines of tasks at once and draw columns separators after;
- - By having each column-print job decoulpled, we avoid havint to measure and compensate lengths of UTF8 strings;
-- [x] Review all code for bugs related to auto-cast on ptrdiff_t (signed/unsigned);
-- [x] Review all code for bugs related to auto-cast on size_t (signed/unsigned);
-- [x] Go over all `TODO` items;
-- [x] Hide stderr messages from app screen.
-- [x] Improve error detection/messages.
diff --git a/ttt.jai b/ttt.jai
new file mode 100644
index 0000000..67d6e18
--- /dev/null
+++ b/ttt.jai
@@ -0,0 +1,1806 @@
+// Copyright 2024 Daniel Almeida Martins
+// License GPL-3.0-or-later
+
+DEBUG :: false;
+
+#import "Basic"()(MEMORY_DEBUGGER=DEBUG); // Enabling memory debug adds ~30MB of RAM.
+#import "System";
+#import "Sort";
+#import "Math";
+#import "File";
+#import "File_Utilities";
+#import "String";
+#import "Saturation"(PREFER_BRANCH_FREE_CODE=true);
+#import "UTF8";
+TUI :: #import "TUI"(COLOR_MODE_BITS=4);
+
+VERSION :: "2.0"; // Use only 3 chars (to fit layouts).
+YEAR :: "2024";
+NUM_WEEK_DAYS :: 7;
+
+APP_FOLDER_NAME :: ".task_time_tracker";
+DB_FILE_NAME :: "database.bin";
+AR_FILE_NAME :: "archive.csv";
+DB_FILE_SIGN_STR :: "TTT:B:02";
+
+ASSERT_NOT_NULL :: "Parameter '%' is null.";
+ASSERT_NOT_EMPTY :: "Parameter '%' is empty.";
+ASSERT_INVALID_INDEX:: "Invalid index '%'.";
+
+SECONDS_IN_MINUTE :: cast(s64)60;
+SECONDS_IN_HOUR :: cast(s64)60*SECONDS_IN_MINUTE;
+SECONDS_IN_DAY :: cast(s64)24*SECONDS_IN_HOUR;
+SECONDS_IN_YEAR :: cast(s64)365*SECONDS_IN_DAY;
+MAX_DATABASE_TASKS :: S64_MAX;
+
+Task :: struct {
+ times : [NUM_WEEK_DAYS] s64;
+ name : [72] u8;
+}
+
+Database :: struct {
+ modified_on : Apollo_Time;
+ active_idx : s64 = -1;
+ selected_idx : s64 = -1;
+ total_times : [NUM_WEEK_DAYS] s64;
+ tasks : [..] Task;
+}
+
+database : Database;
+archive : Database;
+is_autosave_enabled := true;
+countdown_to_autosave := -1;
+first_day_of_week := 1; // (0-6, Sunday = 0, Monday = 1, ...)
+app_directory : string;
+db_file_path : string;
+ar_file_path : string;
+
+size_x : int;
+size_y : int;
+pos_x : int;
+pos_y : int;
+draw_string_builder : String_Builder;
+
+style_default := TUI.Style.{
+ background = TUI.Palette.BLACK,
+ foreground = TUI.Palette.WHITE,
+ use_default_background_color = true,
+ use_default_foreground_color = true,
+};
+
+style_selected := TUI.Style.{
+ background = TUI.Palette.CYAN,
+ foreground = TUI.Palette.BLACK,
+};
+
+style_selected_inverted := TUI.Style.{
+ background = TUI.Palette.BLACK,
+ foreground = TUI.Palette.CYAN,
+ bold = true,
+ use_default_background_color = true,
+};
+
+style_active := TUI.Style.{
+ background = TUI.Palette.BLACK,
+ foreground = TUI.Palette.BLUE,
+ bold = true,
+ use_default_background_color = true,
+};
+
+style_active_selected := TUI.Style.{
+ background = TUI.Palette.BLUE,
+ foreground = TUI.Palette.WHITE,
+ bold = true,
+};
+
+style_error := TUI.Style.{
+ background = TUI.Palette.BLACK,
+ foreground = TUI.Palette.RED,
+ bold = true,
+ use_default_background_color = true,
+};
+
+Layouts :: enum u8 {
+ NORMAL;
+ COMPACT;
+}
+
+
+error_string_builder: String_Builder;
+
+error_time_limit := Apollo_Time.{0, 0};
+
+print_error :: (message: string, data: *void, info: Log_Info) {
+
+ if TUI.is_active() == false {
+ write_strings(message, "\n", to_standard_error = true);
+ return;
+ }
+
+ if error_time_limit < current_time_monotonic() {
+ reset(*error_string_builder);
+ }
+ else {
+ append(*error_string_builder, " | ");
+ }
+ append(*error_string_builder, message);
+ error_time_limit = current_time_monotonic() + seconds_to_apollo(5);
+}
+
+draw_error_window :: () {
+ if error_time_limit < current_time_monotonic() return;
+
+ // Don't show error if main window is too small.
+ w_size_x: int = ifx size_x > 120 then 120 else size_x - 2;
+ w_size_y: int = 3;
+ if (current_time_monotonic() >= error_time_limit
+ || size_x - w_size_x < 2
+ || size_y - w_size_y < 2
+ ) {
+ return;
+ }
+
+ pos_x := 1 + (size_x - w_size_x) / 2;
+ pos_y := 1 + (size_y - w_size_y) / 2;
+
+ TUI.using_style(style_error);
+ TUI.draw_box(pos_x, pos_y, w_size_x, w_size_y, true);
+ TUI.set_cursor_position(pos_x + 1, pos_y);
+ write_string(" Error ");
+ TUI.set_cursor_position(pos_x + 1, pos_y + 1);
+ for 1..w_size_x-2 {
+ print_character(#char " ");
+ }
+ TUI.set_cursor_position(pos_x + 1, pos_y + 1);
+ write_builder(*error_string_builder, false);
+}
+
+trigger_autosave :: () {
+ countdown_to_autosave = 13375; // ms
+}
+
+show_processing :: () {
+ TUI.set_cursor_position(1, 1);
+ TUI.using_style(style_active);
+ write_strings(TUI.Commands.DrawingMode, TUI.Drawings.Diamond, TUI.Commands.TextMode);
+}
+
+hide_processing :: () {
+ TUI.set_cursor_position(1, 1);
+ TUI.using_style(style_default);
+ TUI.tui_write_string(TUI.Commands.DrawingMode);
+ TUI.tui_write_string(TUI.Drawings.CornerTL);
+ TUI.tui_write_string(TUI.Commands.TextMode);
+}
+
+// Returns true if string to_compare is equal to any of the other passed strings, false otherwise.
+is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool {
+ return to_compare == test_a || to_compare == test_b;
+}
+
+// Count digits required to represent number on base. Sign is discarded.
+count_digits :: (number: s64, base: s64 = 10) -> s64 {
+ assert(base > 1, "The smallest integer base for a number system is 2.");
+ digits := 0;
+ while number != 0 {
+ number /= base;
+ digits += 1;
+ }
+ return digits;
+}
+
+// Prints, on row y and column x, the time using 5 characters centered on space.
+// Returns the result of a call to mvprintw.
+print_time :: (y: int, x: int, time: s64, space: int) -> int {
+
+ // Use TUI stubs so that callers may choose to use the tui_builder buffer.
+ print :: TUI.tui_print;
+ write_string :: TUI.tui_write_string;
+
+ TIME_CHARS :: 5;
+ assert(space >= TIME_CHARS);
+
+ mul_f64_s64 :: inline (a: float64, b: s64) -> s64 {
+ return cast(s64)(a * cast(float64)b);
+ }
+
+ left_padding := (space - TIME_CHARS) / 2;
+ right_padding := space - TIME_CHARS - left_padding;
+
+ print_padding :: (size: int) {
+ assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience.");
+ while size > 0 {
+ write_string(" ");
+ size -= 1;
+ }
+ }
+
+ TUI.set_cursor_position(x, y);
+
+ if time < 0 {
+ print_padding(left_padding);
+ write_string(" - ");
+ print_padding(right_padding);
+ return 0;
+ }
+ else if time == 0 {
+ print_padding(left_padding);
+ write_string(" 0 ");
+ print_padding(right_padding);
+ return 0;
+ }
+ else if time < SECONDS_IN_MINUTE {
+ print_padding(left_padding);
+ print("%s ", FormatInt.{value = time, minimum_digits=3, padding=#char " "});
+ print_padding(right_padding);
+ return 0;
+ }
+ else if time < #run mul_f64_s64(100, SECONDS_IN_HOUR) {
+ hours := time / SECONDS_IN_HOUR;
+ minutes := (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE;
+ print_padding(left_padding);
+ print("%:%", FormatInt.{value = hours, minimum_digits=2}, FormatInt.{value = minutes, minimum_digits=2});
+ print_padding(right_padding);
+ return 0;
+ }
+ else if time < #run mul_f64_s64(9999.5, SECONDS_IN_DAY) {
+ value := cast(float64) time / SECONDS_IN_DAY;
+ decimals :=
+ ifx time >= #run mul_f64_s64(99.95, SECONDS_IN_DAY) then 0 else
+ ifx time >= #run mul_f64_s64(9.995, SECONDS_IN_DAY) then 1 else
+ 2;
+ print_padding(left_padding);
+ print("%d", FormatFloat.{value = value, trailing_width=decimals, width=4, zero_removal=.NO});
+ print_padding(right_padding);
+ return 0;
+ }
+ else if time < #run mul_f64_s64(9999.5, SECONDS_IN_YEAR) {
+ value := cast(float64) time / SECONDS_IN_YEAR;
+ decimals :=
+ ifx time >= #run mul_f64_s64(99.95, SECONDS_IN_YEAR) then 0 else
+ ifx time >= #run mul_f64_s64(9.995, SECONDS_IN_YEAR) then 1 else
+ 2;
+ print_padding(left_padding);
+ print("%y", FormatFloat.{value = value, trailing_width=decimals, width=4, zero_removal=.NO});
+ print_padding(right_padding);
+ return 0;
+ }
+ else {
+ print_padding(left_padding);
+ write_string(" ∞ ");
+ print_padding(right_padding);
+ return 0;
+ }
+}
+
+// Returns active task or NULL if none applies.
+get_active_task :: inline (db: Database) -> *Task {
+ return ifx db.active_idx >= 0 then *db.tasks[db.active_idx] else null;
+}
+
+// Returns selected task or NULL if none applies.
+get_selected_task :: inline (db: Database) -> *Task {
+ return ifx db.selected_idx >= 0 then *db.tasks[db.selected_idx] else null;
+}
+
+is_valid_index :: inline(db: Database, index: s64) -> bool { return index >= 0 && index < db.tasks.count; }
+
+// Adds a task to the database and returns it.
+// If necessary, expands database capacity.
+add_task :: (db: *Database, task: *Task = null) -> task: *Task, index: s64 {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+
+ // If the task belongs to this database, calling array_add might invalidate the pointer
+ // because the memory may be reallocated, thus we always use a copy of the task.
+ new_task := ifx task == null then .{} else <<task;
+ array_add(*db.tasks, new_task);
+
+ for * db.total_times {
+ it.* = add(it.*, new_task.times[it_index]);
+ }
+
+ idx := db.tasks.count-1;
+ return *db.tasks[idx], idx;
+}
+
+// Deletes task from database.
+// If possible, shrinks the database capacity.
+// Returns success.
+delete_task :: (using db: *Database, index: s64) -> bool {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(is_valid_index(db, index), ASSERT_INVALID_INDEX, index);
+
+ // Remove task timer values from total timers.
+ for tasks[index].times {
+ total_times[it_index] = sub(total_times[it_index], it);
+ }
+
+ // Move tasks after the index position to their new positions.
+ for index..tasks.count-2
+ tasks[it] = tasks[it+1];
+ tasks.count -= 1;
+
+ // Adjust selected task.
+ if (selected_idx >= tasks.count) {
+ selected_idx -= 1;
+ }
+
+ // Adjust active task.
+ if (active_idx > index) {
+ active_idx -= 1;
+ }
+ else if (active_idx == index) {
+ active_idx = -1;
+ }
+
+ // Try to shrink database capacity if using more than 2MB.
+ size_of_task := size_of(Task);
+ if (tasks.allocated >> 2) > tasks.count && tasks.allocated * size_of_task > 2_000_000 {
+ new_capacity := tasks.allocated >> 1;
+ new_tasks_data := realloc(tasks.data, new_capacity * size_of_task, tasks.allocated * size_of_task,, tasks.allocator);
+ if new_tasks_data != null {
+ tasks.data = new_tasks_data;
+ tasks.allocated = new_capacity;
+ }
+ }
+
+ return true;
+}
+
+// Moves task from source to target.
+// Source and target get clamped to database size.
+move_task :: (using db: *Database, source: s64, target: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+
+ source = clamp(source, 0, tasks.count-1);
+ target = clamp(target, 0, tasks.count-1);
+
+ if (source == target) return;
+
+ // Move task to new location, but first, shift the others to allow some space.
+ temp_task := tasks[source];
+ move_size := abs(target - source);
+
+ if target > source {
+ for 0..move_size-1
+ tasks[source + it] = tasks[source + it + 1];
+ }
+ else {
+ for < move_size-1..0
+ tasks[target + it + 1] = tasks[target + it];
+ }
+
+ tasks[target] = temp_task;
+
+ // Adjust active and selected tasks.
+ if (active_idx == source) {
+ active_idx = target;
+ }
+ else if (source < active_idx && active_idx <= target) {
+ active_idx -= 1;
+ }
+ else if (target <= active_idx && active_idx < source) {
+ active_idx += 1;
+ }
+ selected_idx = target;
+}
+
+// Find similar task and return it's index, or -1 if not found.
+find_similar_task :: (db: *Database, task: Task, ignore_times := false) -> idx: s64 {
+ compare_array :: (a: [] $T, b: [] T) -> int {
+ for 0..min(a.count, b.count)-1 {
+ if a[it] > b[it] return 1;
+ if a[it] < b[it] return -1;
+ }
+ if a.count > b.count return 1;
+ if a.count < b.count return -1;
+ return 0;
+ }
+
+ for db.tasks {
+ if compare(xx task.name, xx it.name) == 0 && (ignore_times || compare_array(task.times, it.times) == 0) {
+ return it_index;
+ }
+ }
+ return -1;
+}
+
+// Updates the times on the active task (and adjusts database totals).
+update_times :: (db: *Database) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+
+ // Get time frame in UTC.
+ start_time := db.modified_on;
+ stop_time := seconds_to_apollo(to_seconds(current_time_consensus())); // HACK Discard sub-seconds information because Task.times only store seconds. To other workaround would be to use Task.times as Apollo_Time instead of s64 seconds.
+
+ // Keep track of this update.
+ db.modified_on = stop_time;
+
+ active_task := get_active_task(db);
+
+ if active_task == null return;
+
+ start_week_day: s8;
+ while (start_time < stop_time) {
+
+ start_week_day = to_calendar(start_time, .LOCAL).day_of_week_starting_at_0;
+
+ // Get next day in local time.
+ start_of_day_cal := to_calendar(start_time, .LOCAL);
+ start_of_day_cal.hour = 0;
+ start_of_day_cal.minute = 0;
+ start_of_day_cal.second = 0;
+ start_of_day_cal.millisecond = 0;
+ start_of_day := calendar_to_apollo(start_of_day_cal);
+
+ next_day := start_of_day + #run seconds_to_apollo(SECONDS_IN_DAY);
+ next_start := ifx next_day < stop_time then next_day else stop_time;
+ elapsed_time := to_seconds(next_start - start_time);
+ active_task.times[start_week_day] += elapsed_time;
+ db.total_times[start_week_day] += elapsed_time;
+
+ start_time = next_start;
+ }
+}
+
+// Recalculates database totals.
+update_total_times :: (db: *Database) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+
+ for *total: db.total_times { <<total = 0; }
+
+ for task: db.tasks {
+ for *total, index: db.total_times {
+ <<total = add(<<total, task.times[index]);
+ }
+ }
+}
+
+// Resets the times of the provided task (and adjusts database totals).
+reset_task_times :: (db: *Database, index: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(is_valid_index(db, index), ASSERT_INVALID_INDEX, index);
+
+ // Make sure we sync before applying the changes.
+ update_times(db);
+
+ for * db.tasks[index].times {
+ db.total_times[it_index] = sub(db.total_times[it_index], <<it);
+ <<it = 0;
+ }
+}
+
+// Sets the time on the day and task provided (and adjusts database totals).
+set_task_time :: (db: *Database, index: s64, day: int, time: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(is_valid_index(db, index), ASSERT_INVALID_INDEX, index);
+
+ // Make sure we sync before applying the changes.
+ update_times(db);
+
+ db.total_times[day] = add(db.total_times[day], time - db.tasks[index].times[day]);
+ db.tasks[index].times[day] = time;
+}
+
+// Adds the time on the day and task provided (and adjusts database totals).
+add_task_time :: (db: *Database, index: s64, day: int, time: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(is_valid_index(db, index), ASSERT_INVALID_INDEX, index);
+
+ // Make sure we sync before applying the changes.
+ update_times(db);
+
+ db.total_times[day] = add(db.total_times[day], time);
+ db.tasks[index].times[day] = add(db.tasks[index].times[day], time);
+}
+
+// Adds the time on the day and task provided (and adjusts database totals).
+add_task_times :: (db: *Database, index: s64, times: [NUM_WEEK_DAYS] s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(is_valid_index(db, index), ASSERT_INVALID_INDEX, index);
+
+ // Make sure we sync before applying the changes.
+ update_times(db);
+
+ for times {
+ db.total_times[it_index] = add(db.total_times[it_index], it);
+ db.tasks[index].times[it_index] = add(db.tasks[index].times[it_index], it);
+ }
+}
+
+// Resets database to the initial state and deallocates all memory taken by tasks.
+reset_database :: (db: *Database) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ array_reset(*db.tasks);
+ <<db = .{};
+}
+
+// Stores data from database into binary file.
+// Returns success.
+store_database :: (db: Database, path: string) -> success: bool #must {
+ assert(xx path, ASSERT_NOT_EMPTY, "path");
+
+ // Open file.
+ file, open_success := file_open(path, for_writing = true);
+ if open_success == false return false;
+ defer file_close(*file);
+
+ file_write(*file, DB_FILE_SIGN_STR);
+ file_write(*file, *db, size_of(Database));
+ file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count);
+
+ return true;
+}
+
+// Loads data from binary file into database.
+// Returns success.
+load_database :: (db: *Database, path: string) -> success: bool #must {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(xx path, ASSERT_NOT_EMPTY, "path");
+
+ // Open file.
+ file, open_success := file_open(path);
+ if open_success == false then return false;
+ defer file_close(*file);
+
+ // Validate file signature.
+ file_signature: [DB_FILE_SIGN_STR.count] u8;
+ read_success := file_read(file, *file_signature, DB_FILE_SIGN_STR.count);
+ if read_success == false log_error("Failed to read file signature.");
+ if cast(string)file_signature != DB_FILE_SIGN_STR {
+ log_error("Invalid file signature while loading database.");
+ return false;
+ }
+
+ // Read database structure.
+ read_success = file_read(file, db, size_of(Database));
+ if read_success == false {
+ log_error("Failed to read database info.");
+ return false;
+ }
+
+ // Reserve database capacity for tasks.
+ tasks_count := db.tasks.count;
+ Initialize(*db.tasks); // Cleanup whatever was read from file.
+ array_reserve(*db.tasks, tasks_count);
+
+ // Read database tasks.
+ file_read(file, db.tasks.data, size_of(Task) * tasks_count);
+ db.tasks.count = tasks_count;
+
+ // Make sure we are reading all the file.
+ buffer: u8;
+ success, bytes := file_read(file, *buffer, 1);
+ if bytes > 0 {
+ log_error("Unexpected content found at the end of file '%'.", path);
+ return false;
+ }
+
+ return true;
+}
+
+// Exports data into CSV file.
+// Returns success.
+export_to_csv :: (db: Database, path: string) -> success: bool #must {
+ assert(xx path, ASSERT_NOT_EMPTY, "path");
+
+ auto_release_temp();
+
+ builder: String_Builder;
+ defer reset(*builder);
+
+ CSV_HEADER :: string.[ "task", "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" ];
+ print_to_builder(*builder, "%\n", join(..CSV_HEADER, separator = ",",, temporary_allocator));
+
+ buffer: [Task.name.count] u8;
+ name: string = xx buffer;
+ for db.tasks {
+ name.count = c_style_strlen(it.name.data);
+ memcpy(name.data, it.name.data, name.count);
+ replace_chars(name, ",", #char " ");
+ print_to_builder(*builder, "%,%,%,%,%,%,%,%\n",
+ name, it.times[0], it.times[1], it.times[2], it.times[3], it.times[4], it.times[5], it.times[6]);
+ }
+
+ return write_entire_file(path, *builder);
+}
+
+// Imports CSV file into database.
+// Returns success.
+import_from_csv :: (db: *Database, path: string) -> bool #must {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(xx path, ASSERT_NOT_EMPTY, "path");
+
+ advance :: inline (array: *[] $T, amount: int = 1) {
+ assert(amount >= 0);
+ assert(array.count >= amount);
+ array.count -= amount;
+ array.data += amount;
+ }
+
+ // Taken from Text_File_Handler module.
+ consume_next_line :: (sp: *string) -> string, bool {
+ s := << sp;
+ found, result, right := split_from_left(s, 10,, temporary_allocator);
+
+ if !found {
+ << sp = "";
+ return s, (s.count > 0);
+ }
+
+ advance(sp, result.count + 1);
+
+ if result {
+ // If there's a carriage return at the end, remove it by decrementing the string's length.
+ if result[result.count-1] == 13 result.count -= 1;
+ }
+
+ return result, true;
+ }
+
+ data, success := read_entire_file(path);
+ if success == false {
+ log_error("Failed to read file '%'.", path);
+ return false;
+ }
+ defer free(data);
+
+ // Work on a string struct copy, otherwise the free(data) will fail.
+ csv := data;
+
+ // Skip header line.
+ consume_next_line(*csv);
+
+ // Parse CSV lines.
+ while (true) {
+ auto_release_temp();
+
+ line, success := consume_next_line(*csv);
+ if success == false then break;
+
+ task: Task;
+ csv_values := split(line, ",",, temporary_allocator);
+
+ // Truncate and import task name.
+ task_name := truncate(csv_values[0], task.name.count);
+ memcpy(task.name.data, task_name.data, task_name.count);
+
+ advance(*csv_values);
+ for csv_values
+ task.times[it_index] = string_to_int(it);
+
+ add_task(db, *task);
+ }
+
+ // Adjust selected task.
+ if (db.selected_idx < 0 && db.tasks.count > 0) db.selected_idx = 0;
+
+ return true;
+}
+
+// Appends task to the end of the CSV file.
+// Returns success.
+append_to_csv :: (task: Task, path: string) -> success: bool #must {
+ assert(xx path, ASSERT_NOT_EMPTY, "path");
+
+ auto_release_temp();
+
+ file, file_success := file_open(path, true, true);
+ if file_success == false then return false;
+ defer file_close(*file);
+
+ file_size := file_length(file);
+ file_set_position(file, file_size-1);
+ last_char: u8;
+ file_read(file, *last_char, 1);
+ if (last_char != #char "\n") {
+ file_write(*file, "\n");
+ }
+
+ task_name := copy_temporary_string(xx task.name);
+ replace_chars(task_name, ",", #char " ");
+ csv_line := tprint("%,%,%,%,%,%,%,%\n",
+ task_name, task.times[0], task.times[1], task.times[2], task.times[3], task.times[4], task.times[5], task.times[6]);
+
+ return file_write(*file, csv_line);
+}
+
+// Selects task by index.
+// Index gets clamped to [0, db->count[.
+select_task :: (db: *Database, index: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ db.selected_idx = ifx db.tasks.count == 0 then -1 else clamp(index, 0, db.tasks.count-1);
+}
+
+// Selects task by delta relative to currently selected task.
+select_task_by_delta :: (db: *Database, delta: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ idx :=
+ ifx (delta > 0 && db.selected_idx > S64_MAX - delta) then S64_MAX else
+ ifx (delta < 0 && db.selected_idx < S64_MIN - delta) then S64_MIN else
+ db.selected_idx + delta;
+ select_task(db, idx);
+}
+
+// Set active task.
+// Passing -1 de-activates any previously active task.
+set_active_task :: (db: *Database, index: s64) {
+ assert(db != null, ASSERT_NOT_NULL, "db");
+ assert(index == -1 || is_valid_index(db, index), ASSERT_INVALID_INDEX, index);
+ update_times(db);
+ db.active_idx = index;
+}
+
+// Returns true when database is full.
+is_database_full :: inline (db: Database) -> bool {
+ return db.tasks.count >= MAX_DATABASE_TASKS;
+}
+
+INPUT_TIMEOUT_MS :: 1000;
+INPUT_AWAIT_INF :: -1;
+
+NUM_HEADER_ROWS :: 1;
+NUM_FOOTER_ROWS :: 1;
+NUM_COLUMNS :: 9;
+
+L_TITLE_IDX :: 0;
+L_DAYS_IDX :: 1;
+L_TOTAL_IDX :: 8;
+
+Column :: struct {
+ header : string;
+ width : int;
+ alignment_offset : int;
+ alignment : u8;
+}
+
+Layout :: struct {
+ columns : [NUM_COLUMNS] Column;
+ archive_title : string;
+}
+
+layouts : [#run type_info(Layouts).values.count] Layout;
+layout_tasks_rows : int;
+is_terminal_too_small := true;
+
+
+initialize_user_interface :: () {
+
+ // Normal layout.
+ layouts[Layouts.NORMAL] = .{
+ archive_title = " Archive ",
+ columns = .[
+ .{ header = #run join(" Task Time Tracker v", VERSION, " "), width = -1, alignment = #char "L" },
+ .{ header = " Sun ", width = 7, alignment = #char "C" },
+ .{ header = " Mon ", width = 7, alignment = #char "C" },
+ .{ header = " Tue ", width = 7, alignment = #char "C" },
+ .{ header = " Wed ", width = 7, alignment = #char "C" },
+ .{ header = " Thu ", width = 7, alignment = #char "C" },
+ .{ header = " Fri ", width = 7, alignment = #char "C" },
+ .{ header = " Sat ", width = 7, alignment = #char "C" },
+ .{ header = " Total ", width = 9, alignment = #char "C" },
+ ]
+ };
+
+ // Compact layout.
+ layouts[Layouts.COMPACT] = .{
+ archive_title = " Archive ",
+ columns = .[
+ .{ header = #run join(" TTT ", VERSION, " "), width = -1, alignment = #char "L" },
+ .{ header = " S ", width = 5, alignment = #char "C" },
+ .{ header = " M ", width = 5, alignment = #char "C" },
+ .{ header = " T ", width = 5, alignment = #char "C" },
+ .{ header = " W ", width = 5, alignment = #char "C" },
+ .{ header = " T ", width = 5, alignment = #char "C" },
+ .{ header = " F ", width = 5, alignment = #char "C" },
+ .{ header = " S ", width = 5, alignment = #char "C" },
+ .{ header = " # ", width = 5, alignment = #char "C" },
+ ]
+ };
+
+ // Calculate alignment_offsets.
+ for * layout: layouts {
+ for * col: layout.columns {
+ offset: int;
+ if col.alignment == {
+ case #char "L";
+ offset = 0;
+ case #char "C";
+ offset = ((col.width - col.header.count) / 2);
+ case #char "R";
+ offset = (col.width - col.header.count);
+ }
+ col.alignment_offset = offset;
+ }
+ }
+
+ assert(TUI.setup_terminal(), "Failed to setup TUI.");
+}
+
+update_layout :: () {
+ // Calculate number of available rows to display tasks.
+ layout_tasks_rows = (size_y - NUM_HEADER_ROWS - NUM_FOOTER_ROWS);
+
+ // Calculate first column width: expands to fill the remaining space dynamically.
+ for * layout: layouts {
+ layout.columns[0].width = size_x - (NUM_COLUMNS - 1) - 2;
+ for 1..layout.columns.count-1 {
+ layout.columns[0].width -= layout.columns[it].width;
+ }
+ }
+}
+
+// Pagination based on currently selected task (show page where selected task is).
+// Display up to rows allowed by the layout, or less if reached end of database.
+get_visible_tasks_indices :: (db: Database) -> first_visible_index: int, last_visible_index: int {
+ first_visible_index :=
+ (db.selected_idx / layout_tasks_rows) * layout_tasks_rows;
+
+ last_visible_index :=
+ first_visible_index +
+ (ifx layout_tasks_rows > db.tasks.count - first_visible_index
+ then db.tasks.count - first_visible_index
+ else layout_tasks_rows);
+
+ return first_visible_index, last_visible_index;
+}
+
+get_day_index_from_layout_index :: inline (layout_index: int) -> int {
+ return (layout_index + first_day_of_week) % NUM_WEEK_DAYS;
+}
+
+// Convert indices to allow using different days as the first-day-of-the-week.
+get_layout_index_from_day_index :: inline (day_index: int) -> int {
+ return (day_index - first_day_of_week + NUM_WEEK_DAYS) % NUM_WEEK_DAYS;
+}
+
+draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) {
+
+ auto_release_temp();
+
+ TUI.using_builder_as_output(*draw_string_builder);
+ print :: TUI.tui_print;
+ write_string :: TUI.tui_write_string;
+
+ // Get context information.
+ active_task := get_active_task(db);
+ selected_task := get_selected_task(db);
+ now_utc := current_time_consensus();
+ now_week_day := to_calendar(now_utc, .LOCAL).day_of_week_starting_at_0;
+
+ // Calculate indices of visible tasks.
+ start_idx, stop_idx := get_visible_tasks_indices(db);
+
+ // If not much is happening, we may just update the active task and it's times.
+ if redraw_all == false {
+
+ if active_task == null then return;
+
+ layout_idx := get_layout_index_from_day_index(now_week_day);
+
+ x_today_offset := 1 + 1 + layout.columns[L_TITLE_IDX].width + 1;
+ for 0..layout_idx-1 {
+ x_today_offset += 1 + layout.columns[L_DAYS_IDX + it].width;
+ }
+
+ x_total_offset := size_x - layout.columns[L_TOTAL_IDX].width;
+
+ // Calculate active task times.
+ task_time := active_task.times[now_week_day];
+ total_task_time := 0;
+ for 0..6 {
+ total_task_time = add(total_task_time, active_task.times[it]);
+ }
+
+ // Draw active task times.
+ if db.active_idx >= start_idx && db.active_idx <= stop_idx {
+ TUI.using_style(ifx db.active_idx == db.selected_idx then style_active_selected else style_active);
+ y := 1 + 1 + (db.active_idx - start_idx);
+ print_time(y, x_today_offset, task_time, layout.columns[L_DAYS_IDX + layout_idx].width);
+ print_time(y, x_total_offset, total_task_time, layout.columns[L_TOTAL_IDX].width);
+ }
+
+ // Calculate daily totals.
+ daily_time := db.total_times[now_week_day];
+ total_time := 0;
+ for 0..6 {
+ total_time = add(total_time, db.total_times[it]);
+ }
+
+ // Draw daily totals.
+ TUI.set_style(style_active);
+ print_time(size_y, x_today_offset, daily_time, layout.columns[L_DAYS_IDX + layout_idx].width);
+ TUI.set_style(style_default);
+ print_time(size_y, x_total_offset, total_time, layout.columns[L_TOTAL_IDX].width);
+
+ write_builder(*draw_string_builder);
+
+ return;
+ }
+
+ x: int;
+ y: int;
+ col: *Column;
+
+ // Reset theme and clear screen.
+ TUI.clear_terminal();
+
+ // Draw outer border.
+ TUI.draw_box(1, 1, size_x, size_y);
+
+ // Draw table grids.
+ y = 1;
+ x = 1;
+ write_string(TUI.Commands.DrawingMode);
+ for 0..layout.columns.count-2 {
+ column := layout.columns[it];
+ x += 1 + column.width;
+ TUI.set_cursor_position(x, y);
+ write_string(TUI.Drawings.TeeT);
+ for row: 2..size_y {
+ TUI.set_cursor_position(x, row);
+ write_string(TUI.Drawings.LineV);
+ }
+ TUI.set_cursor_position(x, size_y);
+ write_string(TUI.Drawings.TeeB);
+ }
+ write_string(TUI.Commands.TextMode);
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw headers.
+ y = 1;
+ x = 1;
+
+ // Headers : title
+ x += 1;
+ col = *layout.columns[L_TITLE_IDX];
+ TUI.set_cursor_position(x + col.alignment_offset, y);
+ write_string(ifx db == *archive then layout.archive_title else col.header);
+ x += col.width;
+
+ // Headers : days
+ for 0..NUM_WEEK_DAYS-1 {
+ day_idx := get_day_index_from_layout_index(it);
+ x += 1;
+
+ // Apply theme.
+ if (day_idx == now_week_day && active_task != null) {
+ TUI.set_style(style_active);
+ }
+ else if (day_idx == now_week_day) {
+ TUI.set_style(style_selected_inverted);
+ }
+ else {
+ TUI.set_style(style_default);
+ }
+ col = *layout.columns[L_DAYS_IDX + day_idx];
+ TUI.set_cursor_position(x + col.alignment_offset, y);
+ write_string(col.header);
+ x += col.width;
+ }
+ TUI.set_style(style_default);
+
+ // Headers : total
+ x += 1;
+ col = *layout.columns[L_TOTAL_IDX];
+ TUI.set_cursor_position(x + col.alignment_offset, y);
+ write_string(col.header);
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw tasks.
+
+ total_time := 0;
+ column_width: int;
+
+ empty_line := talloc_string(size_x);
+ memset(empty_line.data, #char " ", size_x);
+
+ y = 1;
+ for task_idx: start_idx..stop_idx-1 {
+ task := *db.tasks[task_idx];
+ y += 1;
+ x = 1;
+
+ // Apply theme.
+ if (task == active_task && task == selected_task) {
+ TUI.set_style(style_active_selected);
+ }
+ else if (task == selected_task) {
+ TUI.set_style(style_selected);
+ }
+ else if (task == active_task) {
+ TUI.set_style(style_active);
+ }
+ else {
+ TUI.set_style(style_default);
+ }
+
+ // Task title.
+ x += 1;
+ column_width = layout.columns[L_TITLE_IDX].width;
+ // Print title.
+ // When the column is wider than the name, we end up with the trailing zeros.
+ // Thankfully, the trailing zeros are not printed so, it's all good.
+ task_name := cast(string)task.name;
+ task_name = truncate(task_name, column_width);
+ TUI.set_cursor_position(x, y);
+ write_string(task_name);
+ // Paint the remaining column space.
+ task_name_char_count := count_characters(task_name, is_null_terminated = true);
+ paint_remaining := string.{ column_width - task_name_char_count, empty_line.data };
+ write_string(paint_remaining);
+
+ x += column_width;
+
+ // Task times.
+ total_time = 0;
+ for 0..NUM_WEEK_DAYS-1 {
+ x += 1;
+ day_idx := get_day_index_from_layout_index(it);
+ column_width = layout.columns[L_DAYS_IDX + day_idx].width;
+ task_time := task.times[day_idx];
+ total_time = add(total_time, task_time);
+ print_time(y, x, task_time, column_width);
+ x += column_width;
+ }
+
+ // Task total.
+ x += 1;
+ print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width);
+ }
+ TUI.set_style(style_default);
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw selected/total tasks.
+ size := 1 + count_digits(db.selected_idx + 1) + 1 + count_digits(db.tasks.count) + 1; // " XXX/YYY "
+ TUI.set_cursor_position(2, size_y);
+ if (size <= layout.columns[L_TITLE_IDX].width) {
+ print(" %/% ", db.selected_idx + 1, db.tasks.count);
+ }
+ else {
+ print("%", db.selected_idx + 1);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw daily totals.
+ y = size_y;
+ x = 1 + 1 + layout.columns[L_TITLE_IDX].width;
+ total_time = 0;
+ for 0..NUM_WEEK_DAYS-1 {
+ day_idx := get_day_index_from_layout_index(it);
+ daily_total := db.total_times[day_idx];
+ x += 1;
+
+ // Apply theme.
+ if (day_idx == now_week_day && active_task != null) {
+ TUI.set_style(style_active);
+ }
+ else if (day_idx == now_week_day) {
+ TUI.set_style(style_selected_inverted);
+ }
+ else {
+ TUI.set_style(style_default);
+ }
+
+ column_width = layout.columns[L_DAYS_IDX + day_idx].width;
+ total_time = add(total_time, daily_total);
+ print_time(y, x, daily_total, column_width);
+ x += column_width;
+ }
+ TUI.set_style(style_default);
+ x += 1;
+ print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width);
+
+ write_builder(*draw_string_builder);
+}
+
+free_memory :: () {
+ reset_database(*database);
+ reset_database(*archive);
+
+ free(app_directory);
+ free(db_file_path);
+ free(ar_file_path);
+}
+
+read_input_string :: (x: int, y: int, input_limit: int, input_width: int = 0) -> value: string, success: bool {
+ TUI.set_cursor_position(x + input_limit, y);
+ write_string(TUI.Commands.DrawingMode);
+ for 1..input_width {
+ write_string(TUI.Drawings.Checkerboard);
+ }
+ write_string(TUI.Commands.TextMode);
+ TUI.set_cursor_position(x, y);
+
+ style_input := context.tui_style;
+ style_input.underline = true;
+ TUI.using_style(style_input);
+
+ value, key := TUI.read_input_line(input_limit);
+ return value, key == TUI.Keys.Enter;
+}
+
+// Returns success.
+read_input_int :: (y: int, message: string) -> value: int, success: bool {
+ x :: 3;
+
+ // Draw checkerboard.
+ TUI.set_cursor_position(2, y);
+ write_string(TUI.Commands.DrawingMode);
+ for 2..x {
+ print(TUI.Drawings.Checkerboard);
+ }
+ write_string(TUI.Commands.TextMode);
+
+ TUI.set_cursor_position(x, y);
+ write_strings(" ", message, " ");
+
+ input_pos_x := x + message.count + 2;
+ input_width := size_x - input_pos_x;
+
+ str := read_input_string(input_pos_x, y, input_width,, temporary_allocator);
+
+ value, success := parse_int(*str);
+ return value, success;
+}
+
+// Shows message to user and waits for user key press.
+prompt_user_key :: (y: int, message: string) -> TUI.Key {
+ x :: 3;
+
+ // Draw checkerboard.
+ TUI.set_cursor_position(2, y);
+ write_string(TUI.Commands.DrawingMode);
+ for 1..size_x-2 {
+ print(TUI.Drawings.Checkerboard);
+ }
+ write_string(TUI.Commands.TextMode);
+
+ TUI.set_cursor_position(x, y);
+ write_strings(" ", message, " ");
+ return TUI.get_key();
+}
+
+main :: () {
+
+ #if DEBUG { defer report_memory_leaks(); }
+
+ context.logger = print_error;
+
+ defer free_memory();
+
+ { // Initialize app directory.
+ home_dir, success_dir := get_home_directory(); // Returns system owned memory.
+ if success_dir == false {
+ home_dir = ".";
+ }
+
+ home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory.
+
+ if success_path == false {
+ log_error("Failed to find home directory '%'.", home_dir);
+ exit(1);
+ }
+
+ app_directory = join(home_path, "/", APP_FOLDER_NAME);
+ db_file_path = join(app_directory, "/", DB_FILE_NAME);
+ ar_file_path = join(app_directory, "/", AR_FILE_NAME);
+
+ make_directory_if_it_does_not_exist(app_directory, recursive = true);
+ }
+
+ { // Initialize database and archive files if needed.
+ if (file_exists(db_file_path) == false) {
+ if (store_database(database, db_file_path) == false) {
+ log_error("Failed to initialize database.");
+ exit(1);
+ }
+ }
+
+ if (file_exists(ar_file_path) == false) {
+ if (export_to_csv(archive, ar_file_path) == false) {
+ log_error("Failed to initialize archive.");
+ exit(1);
+ }
+ }
+ }
+
+ args := get_command_line_arguments();
+ defer array_reset(*args);
+
+ if args.count > 1 {
+
+ is_exit_requested := false;
+ for 1..args.count-1 {
+ if is_equal_to_any(args[it], "--help", "-h") {
+ write_strings(
+ "Usage: ttt [OPTION]... [FILE]...\n",
+ " -i, --import-csv [FILE] Import CSV file to database (discard first row).\n",
+ " -e, --export-csv [FILE] Export database to CSV file.\n",
+ " -s, --start-of-week [NUMBER] Set first day of week (0=Sunday, 1=Monday...).\n",
+ " -n, --no-autosave Disable autosave feature (only save on exit).\n",
+ " -h, --help Display this help and exit.\n",
+ " -v, --version Output version information and exit.\n",
+ "\n",
+ "In app commands\n",
+ " w, W Archive duplicates and reset all tasks.\n",
+ " a, A Archive selected task (except if active).\n",
+ " r, R Restore selected task from archive.\n",
+ " t, T Select currently active task (if any).\n",
+ " d, D Duplicate selected task.\n",
+ " c, C Coalesce similar tasks.\n",
+ " n, N Create new task.\n",
+ " m, M Move selected task to position.\n",
+ " g, G Select task by position.\n",
+ " i, I Invert tasks order.\n",
+ " s, S Sort tasks by:\n",
+ " n name;\n",
+ " t total time;\n",
+ " 1..7 time of Nth day of week.\n",
+ " q, Q Save changes and exit.\n",
+ " F2 Rename selected task.\n",
+ " F5 Recalculate total times.\n",
+ " TAB Toggle archive view.\n",
+ " BACKSPACE Reset times for selected task.\n",
+ " DELETE Delete selected task (except if active).\n",
+ " SPACE, ENTER Toggle selected task as active/inactive.\n",
+ " 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n",
+ " =# sets # seconds;\n",
+ " -# subtracts # seconds;\n",
+ " # adds # seconds;\n",
+ " #m specifies # as minutes;\n",
+ " #h specifies # as hours;\n",
+ " #d specifies # as days;\n",
+ " #y specifies # as years.\n",
+ " UP Select task above.\n",
+ " DOWN Select task below.\n",
+ " PAGE-UP Select task 1 page above.\n",
+ " PAGE-DOWN Select task 1 page below.\n",
+ " HOME Select first/top task.\n",
+ " END Select last/bottom task.\n",
+ "\n",
+ "Notes\n");
+ print("- All data files are stored in '%'.\n", app_directory);
+ print(" If the home directory is undefined, './%' will be used.\n", APP_FOLDER_NAME);
+ write_strings(
+ " The database tasks are stored in binary format on the 'database.bin' file.\n",
+ " The archived entries are stored in CSV format on the 'archive.csv' file.\n",
+ "- During intensive operations such as saving or recalculating totals times,\n",
+ " a diamond symbol is shown on the top left corner.\n"
+ );
+ exit(0);
+ }
+
+ if is_equal_to_any(args[it], "--version", "-v") {
+ print("Task Time Tracker version % \nCopyright % Daniel Martins\nLicense GPL-3.0-or-later\n", VERSION, YEAR);
+ exit(0);
+ }
+
+ if is_equal_to_any(args[it], "--import-csv", "-i") {
+ it += 1;
+ if it >= args.count {
+ log_error("Missing CSV file path to import.");
+ exit(1);
+ }
+ if (load_database(*database, db_file_path) == false) {
+ log_error("Failed to load database during import.");
+ exit(1);
+ }
+ if (import_from_csv(*database, args[it]) == false) {
+ log_error("Failed to import CSV file.");
+ exit(1);
+ }
+ if (store_database(*database, db_file_path) == false) {
+ log_error("Failed to store database during import.");
+ exit(1);
+ }
+ reset_database(*database);
+ is_exit_requested = true;
+ continue;
+ }
+
+ if is_equal_to_any(args[it], "--export-csv", "-e") {
+ it += 1;
+ if it >= args.count {
+ log_error("Missing CSV file path to export.");
+ exit(1);
+ }
+ if (load_database(*database, db_file_path) == false) {
+ log_error("Failed to load database during export.");
+ exit(1);
+ }
+ if (export_to_csv(*database, args[it]) == false) {
+ log_error("Failed to export CSV file.");
+ exit(1);
+ }
+ reset_database(*database);
+ is_exit_requested = true;
+ continue;
+ }
+
+ if is_equal_to_any(args[it], "--start-of-week", "-s") {
+ it += 1;
+ if it >= args.count {
+ log_error("Missing number for starting day of week.");
+ exit(1);
+ }
+ first_day_of_week = parse_int(*args[it]);
+ continue;
+ }
+
+ if is_equal_to_any(args[it], "--no-autosave", "-n") {
+ is_autosave_enabled = false;
+ continue;
+ }
+
+ log_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]);
+ exit(1);
+ }
+
+ if is_exit_requested {
+ exit(0);
+ }
+ }
+
+ if (load_database(*database, db_file_path) == false) {
+ log_error("Failed to load database.");
+ exit(1);
+ }
+
+ initialize_user_interface();
+
+ db := *database;
+ layout := *layouts[Layouts.COMPACT];
+ redraw_all := true;
+ action_style : TUI.Style;
+
+ TUI.flush_input();
+ TUI.set_next_key(TUI.Keys.Resize);
+ while (true) {
+
+ reset_temporary_storage();
+
+ TUI.set_style(style_default);
+
+ if (is_terminal_too_small) {
+ INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3.";
+ TUI.set_cursor_position((size_x - INVALID_WINDOW_MESSAGE.count) / 2, size_y / 2);
+ write_strings(INVALID_WINDOW_MESSAGE);
+ }
+ else {
+ draw_user_interface(db, layout, redraw_all);
+ draw_error_window();
+ }
+
+ key := TUI.get_key(INPUT_TIMEOUT_MS);
+
+ if key == #char "q" || key == #char "Q" break;
+
+ redraw_all = key != TUI.Keys.None;
+
+ update_times(*database);
+
+ selected_task := get_selected_task(db);
+ active_task := get_active_task(db);
+ selected_task_row: int;
+ {
+ using db;
+ action_style = ifx selected_idx == active_idx && selected_idx != -1 then style_active else style_selected_inverted;
+
+ selected_task_row = ifx is_terminal_too_small then 0
+ else ifx (selected_idx < 0) then 2
+ else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS + 1;
+ }
+
+ if key == {
+
+ // When input times out.
+ case TUI.Keys.None;
+ if (is_autosave_enabled && countdown_to_autosave > 0) {
+ countdown_to_autosave -= INPUT_TIMEOUT_MS;
+ if (countdown_to_autosave <= 0) {
+ show_processing();
+ if (db == *archive) {
+ if export_to_csv(*archive, ar_file_path) == false {
+ log_error("Failed to store archive during autosave.");
+ }
+ }
+ if store_database(database, db_file_path) == false {
+ log_error("Failed to store database during autosave.");
+ }
+ hide_processing();
+ }
+ }
+
+ // When terminal is resized.
+ case TUI.Keys.Resize;
+ TUI.clear_terminal();
+ size_x, size_y = TUI.get_terminal_size();
+ is_terminal_too_small = size_x < 60 || size_y < 3;
+ update_layout();
+ layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT];
+
+ // Invert sort.
+ case #char "i"; #through;
+ case #char "I";
+ if (db.tasks.count <= 1) continue;
+
+ count := db.tasks.count-1;
+ task: Task;
+ for 0..count/2 {
+ task = db.tasks[it];
+ db.tasks[it] = db.tasks[count-it];
+ db.tasks[count-it] = task;
+ }
+ if db.active_idx >= 0
+ db.active_idx = count - db.active_idx;
+ trigger_autosave();
+
+ // New task.
+ case #char "n"; #through;
+ case #char "N";
+ if is_database_full(db) {
+ TUI.using_style(style_error);
+ prompt_user_key(selected_task_row, "Unable to create task: database is full.");
+ continue;
+ }
+
+ // Create new task.
+ now_utc := current_time_consensus();
+ now_local := to_calendar(now_utc, .LOCAL);
+ name := calendar_to_iso_string(now_local);
+ task, index := add_task(db);
+ memcpy(task.name.data, name.data, min(Task.name.count, name.count));
+
+ // Select new task.
+ select_task(db, index);
+ selected_task = get_selected_task(db);
+ trigger_autosave();
+
+ // Force rename action.
+ TUI.flush_input();
+ TUI.set_next_key(TUI.Keys.F2);
+
+ // Rename.
+ case TUI.Keys.F2;
+ if (selected_task == null) continue;
+
+ // Change task name.
+ TUI.using_style(action_style);
+ input := read_input_string(2, selected_task_row, Task.name.count, size_x - 2 - Task.name.count,, temporary_allocator);
+ if is_empty(input) == false {
+ replace_chars(input, "\t\x0B\x0C\r", #char " "); // Replace weird spaces with space.
+ memset(selected_task.name.data, 0, Task.name.count);
+ memcpy(selected_task.name.data, input.data, min(Task.name.count, input.count));
+ trigger_autosave();
+ }
+
+ // Reset task timers.
+ case TUI.Keys.Backspace;
+ if (selected_task == null) continue;
+
+ TUI.using_style(action_style);
+ if (prompt_user_key(selected_task_row, "Press enter to reset task.") == TUI.Keys.Enter) {
+ reset_task_times(db, db.selected_idx);
+ trigger_autosave();
+ }
+
+ case TUI.Keys.Delete;
+ if (selected_task == null || selected_task == active_task) continue;
+
+ TUI.using_style(action_style);
+ if (prompt_user_key(selected_task_row, "Press enter to delete task.") == TUI.Keys.Enter) {
+ delete_task(db, db.selected_idx);
+ trigger_autosave();
+ }
+
+ // Setup time.
+ case #char "1"; #through;
+ case #char "2"; #through;
+ case #char "3"; #through;
+ case #char "4"; #through;
+ case #char "5"; #through;
+ case #char "6"; #through;
+ case #char "7";
+ if (selected_task == null) continue;
+
+ // Prepare position to input time operation.
+ selected_day := cast(int)(key - #char "1");
+ input_width := layout.columns[L_DAYS_IDX + selected_day].width;
+ input_pos_x := 2 + layout.columns[L_TITLE_IDX].width;
+ for 0..selected_day-1 {
+ input_pos_x += 1 + layout.columns[L_DAYS_IDX + it].width;
+ }
+ input_pos_x += 1;
+
+ // Get input string.
+ TUI.using_style(action_style);
+ input := read_input_string(input_pos_x, selected_task_row, input_width,, temporary_allocator);
+
+ // Abort if input if empty.
+ if is_empty(input) continue;
+
+ // Search for assign '=' operator and discard everything before it.
+ assign_idx := find_index_from_left(input, "=");
+ is_assign := assign_idx >= 0;
+ if is_assign advance(*input, assign_idx + 1);
+
+ // Try to parse a number and abort if it fails.
+ input_float, parse_success := string_to_float64(input);
+ if parse_success == false continue;
+
+ // Try to parse a character representing the time multiplier.
+ multiplier: float64 = 1.0;
+ for 0..input.count-1 {
+ ch := to_lower(input[it]);
+ if ch == {
+ case #char "m"; multiplier = xx SECONDS_IN_MINUTE;
+ case #char "h"; multiplier = xx SECONDS_IN_HOUR;
+ case #char "d"; multiplier = xx SECONDS_IN_DAY;
+ case #char "y"; multiplier = xx SECONDS_IN_YEAR;
+ }
+ }
+
+ // Process input and check if it's valid.
+ input_time := input_float * multiplier;
+ if (input_time > xx S64_MAX || input_time < xx S64_MIN) continue;
+
+ // Apply changes.
+ time := cast(s64)input_time;
+ day := get_day_index_from_layout_index(selected_day);
+ if is_assign set_task_time(db, db.selected_idx, day, time);
+ else add_task_time(db, db.selected_idx, day, time);
+
+ trigger_autosave();
+
+ // Move to.
+ case #char "m"; #through;
+ case #char "M";
+ if selected_task == null continue;
+
+ TUI.using_style(action_style);
+ value, success := read_input_int(selected_task_row, "Move to:");
+ if success == false continue;
+ move_task(db, db.selected_idx, value-1); // -1 to adjust for zero based index
+ trigger_autosave();
+
+ // Go to.
+ case #char "g"; #through;
+ case #char "G";
+ if selected_task == null continue;
+
+ TUI.using_style(action_style);
+ value, success := read_input_int(selected_task_row, "Go to:");
+ if success == false continue;
+ target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1;
+ select_task(db, target_index);
+
+ // Duplicate.
+ case #char "d"; #through;
+ case #char "D";
+ if selected_task == null continue;
+
+ if is_database_full(db) {
+ TUI.using_style(style_error);
+ prompt_user_key(selected_task_row, "Unable to duplicate task: database is full.");
+ continue;
+ }
+
+ add_task(db, selected_task);
+ trigger_autosave();
+
+ // Refresh totals.
+ case TUI.Keys.F5;
+ update_total_times(db);
+ trigger_autosave();
+
+ // Go to active task.
+ case #char "t"; #through;
+ case #char "T";
+ if (active_task == null) continue;
+ select_task(db, db.active_idx);
+
+ // Start and stop.
+ case TUI.Keys.Enter; #through;
+ case TUI.Keys.Space;
+ if (db != *database || selected_task == null) continue;
+ set_active_task(db, ifx db.active_idx == db.selected_idx then -1 else db.selected_idx);
+ active_task = get_active_task(db);
+ trigger_autosave();
+
+ // Toggle archive.
+ case TUI.Keys.Tab;
+ if (db == *database) {
+ if (import_from_csv(*archive, ar_file_path) == false) {
+ reset_database(*archive);
+ log_error("Failed to load archive.");
+ continue;
+ }
+ db = *archive;
+ }
+ else {
+ if (export_to_csv(*archive, ar_file_path) == false) {
+ log_error("Failed to store archive.");
+ continue;
+ }
+ reset_database(*archive);
+ db = *database;
+ }
+
+ // Archive task.
+ case #char "a"; #through;
+ case #char "A";
+ if (db != *database || selected_task == null || selected_task == active_task) continue;
+
+ if (append_to_csv(selected_task, ar_file_path) == false) {
+ log_error("Failed to archive task.");
+ continue;
+ }
+ delete_task(db, db.selected_idx);
+ trigger_autosave();
+
+ // Restore task.
+ case #char "r"; #through;
+ case #char "R";
+ if (db != *archive || selected_task == null) continue;
+
+ if is_database_full(*database) {
+ TUI.using_style(style_error);
+ prompt_user_key(selected_task_row, "Unable to restore task: database is full.");
+ continue;
+ }
+
+ add_task(*database, selected_task);
+ delete_task(db, db.selected_idx);
+ trigger_autosave();
+
+ // Sort by.
+ case #char "s"; #through;
+ case #char "S";
+ TUI.using_style(action_style);
+ sort_by := prompt_user_key(selected_task_row, "Sort by (n) name, (1..7) day, or (t) total time.");
+ show_processing();
+
+ sort_procedure: (a: Task, b: Task) -> s64;
+ prev_active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{};
+ if sort_by == {
+ case #char "n"; #through;
+ case #char "N";
+ sort_procedure = (x, y) => compare_strings(xx x.name, xx y.name);
+
+ case #char "t"; #through;
+ case #char "T";
+ sum_total :: inline (t: Task) -> s64 { total: s64; for t.times { total = add(total, it); } return total; }
+ sort_procedure = (x, y) => sum_total(x) - sum_total(y);
+
+ case #char "1"; #through;
+ case #char "2"; #through;
+ case #char "3"; #through;
+ case #char "4"; #through;
+ case #char "5"; #through;
+ case #char "6"; #through;
+ case #char "7";
+ sort_by_idx := cast(int)(sort_by - #char "1");
+ day := get_day_index_from_layout_index(sort_by_idx);
+ if day == {
+ case 0; sort_procedure = (x, y) => x.times[0] - y.times[0];
+ case 1; sort_procedure = (x, y) => x.times[1] - y.times[1];
+ case 2; sort_procedure = (x, y) => x.times[2] - y.times[2];
+ case 3; sort_procedure = (x, y) => x.times[3] - y.times[3];
+ case 4; sort_procedure = (x, y) => x.times[4] - y.times[4];
+ case 5; sort_procedure = (x, y) => x.times[5] - y.times[5];
+ case 6; sort_procedure = (x, y) => x.times[6] - y.times[6];
+ }
+
+ case;
+ continue;
+ }
+ quick_sort(db.tasks, sort_procedure);
+
+ if db.active_idx >= 0 {
+ db.active_idx = find_similar_task(db, prev_active_task);
+ }
+ trigger_autosave();
+
+ // Workspace cleanup.
+ case #char "w"; #through;
+ case #char "W";
+ if (db != *database || db.tasks.count <= 0) continue;
+ TUI.using_style(action_style);
+ if (prompt_user_key(selected_task_row, "Press enter to archive duplicates and reset all.") != TUI.Keys.Enter) continue;
+ show_processing();
+
+ failed_to_archive := false;
+ for db.tasks {
+ if append_to_csv(it, ar_file_path) {
+ reset_task_times(db, it_index);
+ }
+ else {
+ failed_to_archive = true;
+ }
+ }
+ trigger_autosave();
+ if failed_to_archive then log_error("Failed to archive tasks.");
+
+ // Coalesce similar tasks.
+ case #char "c"; #through;
+ case #char "C";
+ if (db.tasks.count <= 0) continue;
+ TUI.using_style(action_style);
+ if (prompt_user_key(selected_task_row, "Press enter to coalesce similar tasks.") != TUI.Keys.Enter) continue;
+ show_processing();
+
+ active_task_idx := db.active_idx;
+ prev_active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{};
+
+ head_idx := 0;
+ while head_idx < db.tasks.count - 1 {
+ tail_idx := db.tasks.count - 1;
+ while tail_idx > head_idx {
+ t_head := *db.tasks[head_idx];
+ t_tail := *db.tasks[tail_idx];
+ if compare(xx t_head.name, xx t_tail.name) == 0 {
+ add_task_times(db, head_idx, db.tasks[tail_idx].times);
+ delete_task(db, tail_idx);
+ }
+ tail_idx -= 1;
+ }
+ head_idx += 1;
+ }
+
+ if active_task_idx >= 0 {
+ db.active_idx = find_similar_task(db, prev_active_task, ignore_times = true);
+ }
+ trigger_autosave();
+
+ case TUI.Keys.Home;
+ select_task(db, 0);
+
+ case TUI.Keys.Up;
+ select_task_by_delta(db, -1);
+
+ case TUI.Keys.PgUp;
+ select_task_by_delta(db, -layout_tasks_rows);
+
+ case TUI.Keys.End;
+ select_task(db, db.tasks.count-1);
+
+ case TUI.Keys.Down;
+ select_task_by_delta(db, 1);
+
+ case TUI.Keys.PgDown;
+ select_task_by_delta(db, layout_tasks_rows);
+ }
+ }
+
+ // Save any unsaved changes.
+ show_processing();
+ if (db == *archive) {
+ while true {
+ if (export_to_csv(archive, ar_file_path) == false) {
+ log_error("Failed to save archive, retry?");
+ draw_error_window();
+ if TUI.get_key() == TUI.Keys.Escape then break;
+ }
+ else break;
+ }
+ }
+ if (countdown_to_autosave > 0 || is_autosave_enabled == false) {
+ while true {
+ if (store_database(database, db_file_path) == false) {
+ log_error("Failed to save database, retry?");
+ draw_error_window();
+ if TUI.get_key() == TUI.Keys.Escape then break;
+ }
+ else break;
+ }
+ }
+
+ assert(TUI.reset_terminal(), "Failed to reset TUI.");
+
+ return;
+}
diff --git a/unused.c b/unused.c
index 718fa71..48d23c7 100644
--- a/unused.c
+++ b/unused.c
@@ -160,7 +160,6 @@ bool add_task(database_t* db, const task_t* task) {
return true;
}
-// TODO Remove this task and move code to delete_task.
// Removes task by index from database. If possible, shrinks database capacity.
// Returns success.
bool remove_task(database_t* db, uint32_t index) {
@@ -189,7 +188,7 @@ bool remove_task(database_t* db, uint32_t index) {
db->capacity = new_capacity;
db->tasks = new_tasks;
db->active_task = db->tasks + selected_task_offset;
- // TODO Validate selected_task (may now be above count.
+ // Validate selected_task (may now be above count.
}
return true;
diff --git a/unused.jai b/unused.jai
new file mode 100644
index 0000000..1f71b37
--- /dev/null
+++ b/unused.jai
@@ -0,0 +1,353 @@
+auto_release_temp(); // automatically release temporary memory.
+print(">%<", S64_MIN + delta, to_standard_error = true);
+print_to_builder(*builder, "Average % us (% / % bytes) ---------", dbg_average/1000, context.temporary_storage.total_bytes_occupied, context.temporary_storage.high_water_mark); // DEBUG
+print("temp [% .. % .. %] \n", context.temporary_storage.data, get_temporary_storage_mark(), context.temporary_storage.data + context.temporary_storage.size-1);
+
+
+// MEASURE PERFORMANCE
+{
+ dbg_average := 0; // DEBUG
+ dbg_count := 0; // DEBUG
+
+ #import "Basic";
+
+ // Cumulative average: CA_n+1 = (x_n+1 + n*CA_n ) / (n + 1)
+ start := current_time_monotonic(); // DEBUG
+ // ...code to be measured...
+ stop := current_time_monotonic(); // DEBUG
+ dbg_sample := to_nanoseconds(stop-start); // DEBUG
+ dbg_average = (dbg_sample + dbg_count * dbg_average) / (dbg_count + 1); // DEBUG
+ dbg_count += 1; // DEBUG
+ print("Average % ns.\n", dbg_average); // DEBUG
+}
+
+// Memory allocator debugging.
+print_owner_allocator :: (tag: string, memory: *void) {
+ owner := "unkown";
+
+ if true == xx context.allocator.proc(.IS_THIS_YOURS, 0, 0, memory, null) then owner = "default";
+ else if true == xx temp.proc(.IS_THIS_YOURS, 0, 0, memory, null) then owner = "temp";
+
+ print("'%' belongs to '%'\n", tag, owner);
+}
+
+// ttt's database debugging.
+print_database :: (db: Database) {
+ for db.tasks {
+ print("% | % : % : % : % : % : % : %\n", cast(string)it.name,
+ it.times[0],
+ it.times[1],
+ it.times[2],
+ it.times[3],
+ it.times[4],
+ it.times[5],
+ it.times[6]
+ );
+ }
+}
+
+// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- //
+
+
+// Average cumulative calculation.
+average: float64 = 0;
+counter: float64 = 0;
+t0 := current_time_monotonic();
+t1 := current_time_monotonic();
+set_cursor_position(2, 47);
+sample := cast(float64)to_nanoseconds(t1-t0)/1000;
+average = (sample + counter * average) / (counter + 1);
+counter += 1;
+print(">%us<", average);
+
+
+// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- //
+
+// Implementation of tcsetattr, tcgetattr, and tcflush using only 'ioctl' which is provided by jai.
+
+#if USE_LIBC {
+
+ libc :: #system_library "libc";
+
+ // 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;
+}
+else {
+
+ // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcsetattr.c.html
+ tcsetattr :: (fd: s32, optional_actions: s32, termios_p : *Terminal_IO_Mode) -> s32 {
+
+ #if OS == .LINUX {
+ TCSETS :: 0x5402;
+ TCSETSW :: 0x5403;
+ TCSETSF :: 0x5404;
+ tcflag_t :: u32;
+ cc_t :: u8;
+ __KERNEL_NCCS :: 19;
+ __kernel_termios :: struct {
+ c_iflag : tcflag_t; // input mode flags
+ c_oflag : tcflag_t; // output mode flags
+ c_cflag : tcflag_t; // control mode flags
+ c_lflag : tcflag_t; // local mode flags
+ c_line : cc_t; // line discipline
+ c_cc : [__KERNEL_NCCS]cc_t; // control characters
+ };
+
+ k_termios: __kernel_termios;
+ cmd: u64;
+ if optional_actions == {
+ case xx Optional_Actions.TCSANOW;
+ cmd = TCSETS;
+
+ case xx Optional_Actions.TCSADRAIN;
+ cmd = TCSETSW;
+
+ case xx Optional_Actions.TCSAFLUSH;
+ cmd = TCSETSF;
+
+ case;
+ return EINVAL;
+ }
+ // k_termios.c_iflag = termios_p.c_iflag & ~IBAUD0;
+ k_termios.c_iflag = xx termios_p.c_iflag;
+ k_termios.c_oflag = xx termios_p.c_oflag;
+ k_termios.c_cflag = xx termios_p.c_cflag;
+ k_termios.c_lflag = xx termios_p.c_lflag;
+ k_termios.c_line = xx termios_p.c_line;
+ // #if _HAVE_C_ISPEED && _HAVE_STRUCT_TERMIOS_C_ISPEED
+ // k_termios.c_ispeed = termios_p->c_ispeed;
+ // #endif
+ // #if _HAVE_C_OSPEED && _HAVE_STRUCT_TERMIOS_C_OSPEED
+ // k_termios.c_ospeed = termios_p->c_ospeed;
+ // #endif
+ memcpy(*k_termios.c_cc[0], *termios_p.c_cc[0], __KERNEL_NCCS * 1);//size_of(cc_t));
+ return ioctl(fd, cmd, *k_termios);
+ }
+ #if OS == .MACOS {
+ // return __ioctl (fd, TIOCSETAF, termios_p);
+ #assert(false, "NOT IMPLEMENTED");
+ }
+ return 0;
+ }
+
+ // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html
+ tcgetattr :: (fd: s32, termios_p: *Terminal_IO_Mode) -> s32 {
+ TCSETS :: 0x5402;
+ TCSETSW :: 0x5403;
+ TCSETSF :: 0x5404;
+ tcflag_t :: u32;
+ cc_t :: u8;
+ __KERNEL_NCCS :: 19;
+ __kernel_termios :: struct {
+ c_iflag : tcflag_t; // input mode flags
+ c_oflag : tcflag_t; // output mode flags
+ c_cflag : tcflag_t; // control mode flags
+ c_lflag : tcflag_t; // local mode flags
+ c_line : cc_t; // line discipline
+ c_cc : [__KERNEL_NCCS]cc_t; // control characters
+ };
+
+
+ // int
+ // __tcgetattr (int fd, struct termios *termios_p)
+ // {
+ // struct __kernel_termios k_termios;
+ k_termios: __kernel_termios;
+ retval: int;
+ retval = ioctl(fd, TCGETS, *k_termios);
+ if retval == 0 {
+ termios_p.c_iflag = xx k_termios.c_iflag;
+ termios_p.c_oflag = xx k_termios.c_oflag;
+ termios_p.c_cflag = xx k_termios.c_cflag;
+ termios_p.c_lflag = xx k_termios.c_lflag;
+ termios_p.c_line = xx k_termios.c_line;
+ // #if _HAVE_STRUCT_TERMIOS_C_ISPEED
+ // # if _HAVE_C_ISPEED
+ // termios_p->c_ispeed = k_termios.c_ispeed;
+ // # else
+ // termios_p->c_ispeed = k_termios.c_cflag & (CBAUD | CBAUDEX);
+ // # endif
+ // #endif
+ // #if _HAVE_STRUCT_TERMIOS_C_OSPEED
+ // # if _HAVE_C_OSPEED
+ // termios_p->c_ospeed = k_termios.c_ospeed;
+ // # else
+ // termios_p->c_ospeed = k_termios.c_cflag & (CBAUD | CBAUDEX);
+ // # endif
+ // #endif
+ size_of_cc_t := __KERNEL_NCCS * 1;
+ memcpy(*termios_p.c_cc[0], *k_termios.c_cc[0], size_of_cc_t);
+ // memset(*termios_p.c_cc[0] + size_of_cc_t + 1, _POSIX_VDISABLE, (NCCS - __KERNEL_NCCS) * 1);
+ //
+ // if (sizeof (cc_t) == 1 || _POSIX_VDISABLE == 0 || (unsigned char) _POSIX_VDISABLE == (unsigned char) -1) {
+ // memset (__mempcpy (&termios_p->c_cc[0], &k_termios.c_cc[0], __KERNEL_NCCS * sizeof (cc_t)), _POSIX_VDISABLE, (NCCS - __KERNEL_NCCS) * sizeof (cc_t));
+ // }
+ // else
+ // {
+ // memcpy (&termios_p->c_cc[0], &k_termios.c_cc[0], __KERNEL_NCCS * sizeof (cc_t));
+ // for (size_t cnt = __KERNEL_NCCS; cnt < NCCS; ++cnt) {
+ // termios_p->c_cc[cnt] = _POSIX_VDISABLE;
+ // }
+ // }
+ }
+ return xx retval;
+ }
+
+ // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcflush.c.html
+ tcflush :: inline (fd: s32, queue_selector: s32) -> s32 {
+ TCFLSH :: 0x540B;
+ return ioctl(fd, TCFLSH, queue_selector);
+ }
+}
+
+// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- //
+
+
+Step_Iterator :: struct {
+ min: int;
+ max: int;
+ step: int;
+}
+
+step_iterator :: (min: int, max: int, step: int) -> Step_Iterator {
+ return .{ min, max, step };
+}
+
+for_expansion :: (iterator: Step_Iterator, body: Code, flags: For_Flags) #expand {
+ iteration_count: int;
+ for <=cast(bool)(flags & .REVERSE) i: iterator.min..iterator.max {
+ iteration_count += 1;
+ if iteration_count % iterator.step == 0 continue;
+
+ `it := i;
+ `it_index := void;
+
+ #insert body;
+ }
+}
+
+for step_iterator(0, 10, 2) {
+ log("%", it);
+}
+
+
+// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- //
+
+
+// Check what's going on with the temp allocator:
+// Is it really the responsible for these paths?
+// It seems that the next beta (after 0.1.055b) compiler allows us to check this pretty easily.
+//
+//
+// An example that uses several different allocators, then asks them all
+// who owns which memory.
+//
+// Note that this is probably not the kind of thing you want to do at runtime
+// in the steady state, as it may not be very fast, but it could be a very helpful
+// debugging facility.
+//
+
+#import "Basic";
+#import "Pool";
+#import "Flat_Pool";
+#import "rpmalloc";
+
+main :: () {
+ pool: Pool;
+ flat: Flat_Pool;
+
+ a := context.default_allocator;
+ b := Allocator.{pool_allocator_proc, *pool};
+ c := Allocator.{flat_pool_allocator_proc, *flat};
+ d := Allocator.{rpmalloc_allocator_proc, null};
+
+ d.proc(.STARTUP, 0, 0, null, null); // rpmalloc needs explicit init right now, but others don't.
+
+ ma := alloc(1000, allocator=a);
+ mb := alloc(1000, allocator=b);
+ mc := alloc(1000, allocator=c);
+ md := alloc(1000, allocator=d);
+
+ report_who_owns(ma, a, b, c, d);
+ report_who_owns(mb, a, b, c, d);
+ report_who_owns(mc, a, b, c, d);
+ report_who_owns(md, a, b, c, d);
+}
+
+report_who_owns :: (memory: *void, allocators: .. Allocator) {
+ someone_owns_this := false;
+
+ print("Querying all allocators for address: %\n", memory);
+
+ for allocators {
+ caps, name := get_capabilities(it);
+ assert((caps & .IS_THIS_YOURS) != 0); // It had better be claiming to support this!
+
+ yours := cast(bool) it.proc(.IS_THIS_YOURS, 0, 0, memory, it.data);
+ print("[%] says \"%\"\n", yours, name);
+
+ someone_owns_this ||= yours;
+ }
+
+ assert(someone_owns_this);
+}
+
+
+// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- //
+
+
+checked_add :: (a: $T, b: T) -> result: T, overflow: bool
+#modify {
+ if T.type == .INTEGER return;
+ T = null;
+}
+{
+ overflow: bool;
+ result: T = a + b;
+
+ info := type_info(T);
+ if info.signed {
+ // (+A) + (+B) = −C
+ // (−A) + (−B) = +C
+ if ((a > 0) && (b > 0) && (result < 0)) || ((a < 0) && (b < 0) && (result > 0)) {
+ overflow = true;
+ }
+ } else {
+ if result < a {
+ overflow = true;
+ }
+ }
+
+ return result, overflow;
+}
+
+checked_sub :: (a: $T, b: T) -> result: T, overflow: bool
+#modify {
+ if T.type == .INTEGER return;
+ T = null;
+}
+{
+ overflow: bool;
+ result: T = a - b;
+
+ info := type_info(T);
+ if info.signed {
+ // (+A) − (−B) = −C
+ // (−A) − (+B) = +C
+ if ((a > 0) && (b < 0) && (result < 0)) || ((a < 0) && (b > 0) && (result > 0)) {
+ overflow = true;
+ }
+ } else {
+ if result > a {
+ overflow = true;
+ }
+ }
+
+ return result, overflow;
+}