diff options
| author | dam <dam@gudinoff> | 2024-05-29 12:50:48 +0100 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2024-05-29 12:50:48 +0100 |
| commit | 986c0ca11d45e83e97479fcfad5facd1e56b0beb (patch) | |
| tree | 8576d455c6748a38e81787b308fb8cbbe1ad7b89 | |
| parent | 393e5a926cd105c4a2f902824a233cc41af91198 (diff) | |
| parent | ec706533ca26d49670adb97617df0d565528e395 (diff) | |
| download | task-time-tracker-2.0.tar.zst task-time-tracker-2.0.zip | |
Merge with jai-prototypev2.0
| -rw-r--r-- | .hgignore | 1 | ||||
| -rw-r--r-- | LICENSE-GPL-3.0-or-later (renamed from COPYING) | 0 | ||||
| -rw-r--r-- | README.md | 128 | ||||
| -rw-r--r-- | modules/LICENSE-ISC | 15 | ||||
| -rw-r--r-- | modules/LICENSE-MIT | 21 | ||||
| -rw-r--r-- | modules/README.md | 34 | ||||
| -rw-r--r-- | modules/Saturation/module.jai | 418 | ||||
| -rw-r--r-- | modules/Saturation/tests.jai | 647 | ||||
| -rw-r--r-- | modules/TUI/examples/snake.jai | 188 | ||||
| -rw-r--r-- | modules/TUI/key_map.jai | 505 | ||||
| -rw-r--r-- | modules/TUI/module.jai | 805 | ||||
| -rw-r--r-- | modules/TUI/palette_24b.jai | 50 | ||||
| -rw-r--r-- | modules/TUI/palette_4b.jai | 19 | ||||
| -rw-r--r-- | modules/TUI/palette_8b.jai | 307 | ||||
| -rw-r--r-- | modules/TUI/tests.jai | 232 | ||||
| -rw-r--r-- | modules/TUI/unix.jai | 319 | ||||
| -rw-r--r-- | modules/TUI/windows.jai | 390 | ||||
| -rw-r--r-- | modules/UTF8/module.jai | 149 | ||||
| -rw-r--r-- | modules/UTF8/tests.jai | 162 | ||||
| -rw-r--r-- | readme.md | 77 | ||||
| -rw-r--r-- | ttt.jai | 1806 | ||||
| -rw-r--r-- | unused.c | 3 | ||||
| -rw-r--r-- | unused.jai | 353 |
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. @@ -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; +} @@ -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; +} |
