diff options
| author | dam <dam@gudinoff> | 2024-02-28 00:03:27 +0000 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2024-02-28 00:03:27 +0000 |
| commit | d828e742d7702c7e2698db4371012a49dfeb95d5 (patch) | |
| tree | 23c0004ca8e421f79978a5b06e817ffd960a1788 /modules | |
| parent | 6a28cf6fb30f96d540b3ecbfd53b9e20ef61869d (diff) | |
| download | task-time-tracker-d828e742d7702c7e2698db4371012a49dfeb95d5.tar.zst task-time-tracker-d828e742d7702c7e2698db4371012a49dfeb95d5.zip | |
Moved custom modules to newly supported local modules folder.
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/Integer_Saturating_Arithmetic.jai | 416 | ||||
| -rw-r--r-- | modules/TUI/module.jai | 823 | ||||
| -rw-r--r-- | modules/TUI/unix.jai | 286 | ||||
| -rw-r--r-- | modules/TUI/windows.jai | 406 |
4 files changed, 1931 insertions, 0 deletions
diff --git a/modules/Integer_Saturating_Arithmetic.jai b/modules/Integer_Saturating_Arithmetic.jai new file mode 100644 index 0000000..74643e0 --- /dev/null +++ b/modules/Integer_Saturating_Arithmetic.jai @@ -0,0 +1,416 @@ +// Integer saturating arighmetic (with assembly branch-free procedures for x64 - expecting signed values in two's complement). + +#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, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +{ + + #if USE_GENERIC || 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, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +{ + + #if USE_GENERIC || 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, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +{ + + #if USE_GENERIC || 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, $USE_GENERIC: bool = false) -> result: $Tr, remainder: Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +{ + + #if USE_GENERIC || 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 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/TUI/module.jai b/modules/TUI/module.jai new file mode 100644 index 0000000..d986eaf --- /dev/null +++ b/modules/TUI/module.jai @@ -0,0 +1,823 @@ +// TODO Move TUI into ./modules/TUI so we can stop calling --import_dir on compile. +#if OS == { + case .LINUX; + #load "unix.jai"; + case .MACOS; + #load "unix.jai"; + case .WINDOWS; + #load "windows.jai"; + case; + #assert(false, "Unsupported OS."); +} + +#import "Basic"; +#import "String"; +#import "Thread"; + +// Special Graphics Characters +Drawings :: struct { + 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"; +} + +Commands :: struct { + AlternateScreenBuffer :: "\e[?1049h"; + MainScreenBuffer :: "\e[?1049l"; + + DrawingMode :: "\e(0"; + TextMode :: "\e(B"; + ClearScreen :: "\e[2J"; + ClearLine :: "\e[2K"; + ClearScrollBack :: "\e[3J"; + + Bell :: "\x07"; + + SetWindowTitle :: "\e]0;%\e\\"; + + RefreshWindow :: "\e[7t"; // TODO Not yet tested. + + SetIEC2022 :: "\e%@"; + SetUTF8 :: "\e%G"; + + SetGraphicsRendition :: "\e[%m"; + + // Cursor Position + SetCursorPosition :: "\e[%;%H"; + + // Cursor Visibility + ShowCursor :: "\e[?25h"; + HideCursor :: "\e[?25l"; + StartBlinking :: "\e[?25h"; + StopBlinking :: "\e[?25l"; + SaveCursorPosition :: "\e7"; + RestoreCursorPosition :: "\e8"; + + // 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"; + + // Query State + QueryCursorPosition :: "\e[6n"; // Emits the cursor position as: "ESC [ <r> ; <c> R" Where <r> = row and <c> = column. + QueryDeviceAttributes :: "\e[0c"; + QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 <r> ; <c> t" Where <r> = row and <c> = column. TODO Does not work on windows. +} + +clear_style :: () { + write_string(#run sprint(Commands.SetGraphicsRendition, "0")); +} + +Colors8b :: struct { + 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; +} + +// TODO Maybe rename. +set_style_colors :: (foreground: u8, background: u8) { + print( + #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), + foreground, background); +} + +// set_colors_24b :: (foreground_r: u255) // TODO https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit + +set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, negative: bool = false) { + print( + #run sprint(Commands.SetGraphicsRendition, "%;%;%;%"), + ifx bold then 1 else 22, + ifx underline then 4 else 24, + ifx strike_through then 9 else 29, + ifx negative then 7 else 27); +} + +// TODO Maybe make the OS_* procedures as inline?! +// TODO Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. + +/* + 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; // Terminal key-codes have 1 to 6 bytes so we'll use 8 bytes. + +KEY_SIZE :: #run type_info(Key).runtime_size; + +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"; + + Up : Key : #run to_key("\e[A"); + Down : Key : #run to_key("\e[B"); + Right : Key : #run to_key("\e[C"); + Left : Key : #run to_key("\e[D"); + + MetaUp : Key : #run to_key("\e[1;1A"); + MetaDown : Key : #run to_key("\e[1;1B"); + MetaRight : Key : #run to_key("\e[1;1C"); + MetaLeft : Key : #run to_key("\e[1;1D"); + + ShiftUp : Key : #run to_key("\e[1;2A"); + ShiftDown : Key : #run to_key("\e[1;2B"); + ShiftRight : Key : #run to_key("\e[1;2C"); + ShiftLeft : Key : #run to_key("\e[1;2D"); + + AltUp : Key : #run to_key("\e[1;3A"); + AltDown : Key : #run to_key("\e[1;3B"); + AltRight : Key : #run to_key("\e[1;3C"); + AltLeft : Key : #run to_key("\e[1;3D"); + + AltShiftUp : Key : #run to_key("\e[1;4A"); + AltShiftDown : Key : #run to_key("\e[1;4B"); + AltShiftRight : Key : #run to_key("\e[1;4C"); + AltShiftLeft : Key : #run to_key("\e[1;4D"); + + CtrlUp : Key : #run to_key("\e[1;5A"); + CtrlDown : Key : #run to_key("\e[1;5B"); + CtrlRight : Key : #run to_key("\e[1;5C"); + CtrlLeft : Key : #run to_key("\e[1;5D"); + + CtrlShiftUp : Key : #run to_key("\e[1;6A"); + CtrlShiftDown : Key : #run to_key("\e[1;6B"); + CtrlShiftRight : Key : #run to_key("\e[1;6C"); + CtrlShiftLeft : Key : #run to_key("\e[1;6D"); + + CtrlAltUp : Key : #run to_key("\e[1;7A"); + CtrlAltDown : Key : #run to_key("\e[1;7B"); + CtrlAltRight : Key : #run to_key("\e[1;7C"); + CtrlAltLeft : Key : #run to_key("\e[1;7D"); + + CtrlAltShiftUp : Key : #run to_key("\e[1;7A"); + CtrlAltShiftDown : Key : #run to_key("\e[1;7B"); + CtrlAltShiftRight : Key : #run to_key("\e[1;7C"); + CtrlAltShiftLeft : Key : #run to_key("\e[1;7D"); + + Home : Key : #run to_key("\e[H"); + End : Key : #run to_key("\e[F"); + + Escape : Key : 0x00000000_0000001B; + Backspace : Key : 0x00000000_0000007F; + Pause : Key : 0x00000000_0000001A; + Insert : Key : #run to_key("\e[2~"); + Delete : Key : #run to_key("\e[3~"); + PgUp : Key : #run to_key("\e[5~"); + PgDown : Key : #run to_key("\e[6~"); + + /* TODO On get_key, convert F1 to F4 into the format "\e[1X~" + so that: + F1 : 1b 4f 50 : ^OP : -> \e[11~ + Shift+ F1 : 1b 4f 32 50 : ^O2P : -> \e[11;2~ + F2 : 1b 4f 51 : ^OQ : -> \e[12~ + Shift+ F2 : 1b 4f 32 51 : ^O2Q : -> \e[12;2~ + F3 : 1b 4f 52 : ^OR : -> \e[13~ + Shift+ F3 : 1b 4f 32 52 : ^O2R : -> \e[13;2~ + F4 : 1b 4f 53 : ^OS : -> \e[14~ + Meta+ Fx : 1b 4f 31 53 : ^O1S : -> \e[14;1~ + Shift+ F4 : 1b 4f 32 53 : ^O2S : -> \e[14;2~ + Alt+ F4 : 1b 4f 33 53 : ^O3S : -> \e[14;3~ + S+A F4 : 1b 4f 34 53 : ^O4S : -> \e[14;4~ + Ctrl+ F4 : 1b 4f 35 53 : ^O4S : -> \e[14;5~ + Ctrl+ F4 : 1b 4f 35 53 : ^O4S : -> \e[14;5~ + ... + */ + + // WIP HERE + + F1 : Key : #run to_key("\eOP"); + F2 : Key : #run to_key("\eOQ"); + F3 : Key : #run to_key("\eOR"); + F4 : Key : #run to_key("\eOS"); + F5 : Key : #run to_key("\e[15~"); + F6 : Key : #run to_key("\e[17~"); + F7 : Key : #run to_key("\e[18~"); + F8 : Key : #run to_key("\e[19~"); + F9 : Key : #run to_key("\e[20~"); + F10 : Key : #run to_key("\e[21~"); + F11 : Key : #run to_key("\e[23~"); + F12 : Key : #run to_key("\e[24~"); + + MetaF1 : Key : #run to_key("\e[1;1P"); + MetaF2 : Key : #run to_key("\e[1;1Q"); + MetaF3 : Key : #run to_key("\e[1;1R"); + MetaF4 : Key : #run to_key("\e[1;1S"); + MetaF5 : Key : #run to_key("\e[15;1~"); + MetaF6 : Key : #run to_key("\e[17;1~"); + MetaF7 : Key : #run to_key("\e[18;1~"); + MetaF8 : Key : #run to_key("\e[19;1~"); + MetaF9 : Key : #run to_key("\e[20;1~"); + MetaF10 : Key : #run to_key("\e[21;1~"); + MetaF11 : Key : #run to_key("\e[23;1~"); + MetaF12 : Key : #run to_key("\e[24;1~"); + + ShiftF1 : Key : #run to_key("\e[1;2P"); + ShiftF2 : Key : #run to_key("\e[1;2Q"); + ShiftF3 : Key : #run to_key("\e[1;2R"); + ShiftF4 : Key : #run to_key("\e[1;2S"); + ShiftF5 : Key : #run to_key("\e[15;2~"); + ShiftF6 : Key : #run to_key("\e[17;2~"); + ShiftF7 : Key : #run to_key("\e[18;2~"); + ShiftF8 : Key : #run to_key("\e[19;2~"); + ShiftF9 : Key : #run to_key("\e[20;2~"); + ShiftF10 : Key : #run to_key("\e[21;2~"); + ShiftF11 : Key : #run to_key("\e[23;2~"); + ShiftF12 : Key : #run to_key("\e[24;2~"); + + AltF1 : Key : #run to_key("\e[1;3P"); + AltF2 : Key : #run to_key("\e[1;3Q"); + AltF3 : Key : #run to_key("\e[1;3R"); + AltF4 : Key : #run to_key("\e[1;3S"); + AltF5 : Key : #run to_key("\e[15;3~"); + AltF6 : Key : #run to_key("\e[17;3~"); + AltF7 : Key : #run to_key("\e[18;3~"); + AltF8 : Key : #run to_key("\e[19;3~"); + AltF9 : Key : #run to_key("\e[20;3~"); + AltF10 : Key : #run to_key("\e[21;3~"); + AltF11 : Key : #run to_key("\e[23;3~"); + AltF12 : Key : #run to_key("\e[24;3~"); + + AltShiftF1 : Key : #run to_key("\e[1;4P"); + AltShiftF2 : Key : #run to_key("\e[1;4Q"); + AltShiftF3 : Key : #run to_key("\e[1;4R"); + AltShiftF4 : Key : #run to_key("\e[1;4S"); + AltShiftF5 : Key : #run to_key("\e[15;4~"); + AltShiftF6 : Key : #run to_key("\e[17;4~"); + AltShiftF7 : Key : #run to_key("\e[18;4~"); + AltShiftF8 : Key : #run to_key("\e[19;4~"); + AltShiftF9 : Key : #run to_key("\e[20;4~"); + AltShiftF10 : Key : #run to_key("\e[21;4~"); + AltShiftF11 : Key : #run to_key("\e[23;4~"); + AltShiftF12 : Key : #run to_key("\e[24;4~"); + + CtrlF1 : Key : #run to_key("\e[1;5P"); + CtrlF2 : Key : #run to_key("\e[1;5Q"); + CtrlF3 : Key : #run to_key("\e[1;5R"); + CtrlF4 : Key : #run to_key("\e[1;5S"); + CtrlF5 : Key : #run to_key("\e[15;5~"); + CtrlF6 : Key : #run to_key("\e[17;5~"); + CtrlF7 : Key : #run to_key("\e[18;5~"); + CtrlF8 : Key : #run to_key("\e[19;5~"); + CtrlF9 : Key : #run to_key("\e[20;5~"); + CtrlF10 : Key : #run to_key("\e[21;5~"); + CtrlF11 : Key : #run to_key("\e[23;5~"); + CtrlF12 : Key : #run to_key("\e[24;5~"); + + CtrlShiftF1 : Key : #run to_key("\e[1;6P"); + CtrlShiftF2 : Key : #run to_key("\e[1;6Q"); + CtrlShiftF3 : Key : #run to_key("\e[1;6R"); + CtrlShiftF4 : Key : #run to_key("\e[1;6S"); + CtrlShiftF5 : Key : #run to_key("\e[15;6~"); + CtrlShiftF6 : Key : #run to_key("\e[17;6~"); + CtrlShiftF7 : Key : #run to_key("\e[18;6~"); + CtrlShiftF8 : Key : #run to_key("\e[19;6~"); + CtrlShiftF9 : Key : #run to_key("\e[20;6~"); + CtrlShiftF10 : Key : #run to_key("\e[21;6~"); + CtrlShiftF11 : Key : #run to_key("\e[23;6~"); + CtrlShiftF12 : Key : #run to_key("\e[24;6~"); + + CtrlAltF1 : Key : #run to_key("\e[1;7P"); + CtrlAltF2 : Key : #run to_key("\e[1;7Q"); + CtrlAltF3 : Key : #run to_key("\e[1;7R"); + CtrlAltF4 : Key : #run to_key("\e[1;7S"); + CtrlAltF5 : Key : #run to_key("\e[15;7~"); + CtrlAltF6 : Key : #run to_key("\e[17;7~"); + CtrlAltF7 : Key : #run to_key("\e[18;7~"); + CtrlAltF8 : Key : #run to_key("\e[19;7~"); + CtrlAltF9 : Key : #run to_key("\e[20;7~"); + CtrlAltF10 : Key : #run to_key("\e[21;7~"); + CtrlAltF11 : Key : #run to_key("\e[23;7~"); + CtrlAltF12 : Key : #run to_key("\e[24;7~"); + + CtrlAltShiftF1 : Key : #run to_key("\e[1;8P"); + CtrlAltShiftF2 : Key : #run to_key("\e[1;8Q"); + CtrlAltShiftF3 : Key : #run to_key("\e[1;8R"); + CtrlAltShiftF4 : Key : #run to_key("\e[1;8S"); + CtrlAltShiftF5 : Key : #run to_key("\e[15;8~"); + CtrlAltShiftF6 : Key : #run to_key("\e[17;8~"); + CtrlAltShiftF7 : Key : #run to_key("\e[18;8~"); + CtrlAltShiftF8 : Key : #run to_key("\e[19;8~"); + CtrlAltShiftF9 : Key : #run to_key("\e[20;8~"); + CtrlAltShiftF10 : Key : #run to_key("\e[21;8~"); + CtrlAltShiftF11 : Key : #run to_key("\e[23;8~"); + CtrlAltShiftF12 : Key : #run to_key("\e[24;8~"); +} + +to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { + k: Key; + // #if DEBUG { + // assert(str.count <= 8); // TODO Add DEBUG to module parameters. + // } + for 0..str.count-1 #no_abc { + k |= ((cast(u64)str[it]) << (it*8)); + } + return k; +} + +to_string :: inline (key: Key) -> string { // TODO FIXME TEMPORARY MEMORY + str := talloc_string(KEY_SIZE); + str.count = 0; + while key != 0 #no_abc { + str[str.count] = xx key & 0xFF; + key >>= 8; + str.count += 1; + } + return str; +} + +is_escape_code :: inline (key: Key) -> bool { + result := false; + while key != 0 { + key >>= 8; + result |= ((key ^ Keys.Escape) == 0); + } + return result; +} + +initialized := false; + +//input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! +input_buffer : [8] u8; // TODO FIXME Input buffer is too small!!! +input_string : string; +input_override : Key; + +#run { + // TODO FIXME DEBUG HACK or maybe... let it be?! + // Some tests. + assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire terminal (6 bytes) or UTF8 (4 bytes) code."); +} + +assert_is_initialized :: inline () { + assert(initialized, "TUI is not ready."); +} + +set_next_key :: (key: Key) { + assert_is_initialized(); + input_override = key; +} + +get_key :: (timeout_milliseconds: s32 = -1) -> Key { + assert_is_initialized(); + /* + TODO + get_key already deals with utf8 codes, but we don't know when we're receiving ANSI escape codes. If initial key is escape and other keys are awaiting in the input buffer, we need to parse them as escaped sequences. See wikiedia* for help on that. Lets use the escape sequences used on windows amd forget all others. Those should be the most used ones; at least they are the cross-platform compatible ones :P + * https://en.m.wikipedia.org/wiki/ANSI_escape_code + + Check this + https://devmemo.io/cheatsheets/terminal_escape_code/ + */ + + + // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte + is_utf8_continuation_byte :: inline (byte: u8) -> bool { + return (byte & 0xC0) == 0x80; + } + + // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte + // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte + // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte + count_utf8_bytes :: inline (byte: u8) -> int { + if (byte & 0xE0) == 0xC0 return 1+1; + if (byte & 0xF0) == 0xE0 return 1+2; + if (byte & 0xF8) == 0xF0 return 1+3; + return 1; + } + + if input_override != xx Keys.None { + defer input_override = xx Keys.None; + return input_override; + } + + if OS_was_terminal_resized() return xx Keys.Resize; + + should_read_input := false; + is_input_available := false; + + if input_string.count == 0 { + should_read_input = true; + is_input_available = OS_wait_for_input(timeout_milliseconds); + } + else if input_string.count < KEY_SIZE { + should_read_input = true; + is_input_available = OS_wait_for_input(0); + } + + if OS_was_terminal_resized() return xx Keys.Resize; + + if should_read_input && is_input_available { + // Copy buffered bytes to the start, and read the remaining ones from input. + 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; + } + + if input_string.count > 0 + { + utf8_bytes := count_utf8_bytes(input_string[0]); + to_parse := input_string; + to_parse.count = utf8_bytes; + + // Must be a terminal escape sequence. + if utf8_bytes == 1 && input_string[0] == #char "\e" { + assert(input_string.count <= KEY_SIZE, "Received oversized terminal sequence."); // TODO + to_parse.count = ifx input_string.count > KEY_SIZE then KEY_SIZE else input_string.count; // TODO We should look into the input_string and search for the following escape sequence or somehting!? + } + + key := to_key(to_parse); + advance(*input_string, to_parse.count); + return key; + } + + // TODO try_parse_escape_code + // { + // assert(false, "TODO try_parse_escape_code"); + // } + + return xx Keys.None; +} + +// TODO Review me! +read_input :: (count_limit: int = -1, terminators: .. u8) -> string { + assert_is_initialized(); + + assert(count_limit >= 0 || terminators.count > 0, "Infinite loop detected, aborting."); // TODO Maybe just return!? + + 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); + } + 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; + } +} + +// TODO UNTESTED +read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { + assert(count_limit >= 0, "Invalid value on count_limit parameter."); + str := alloc_string(count_limit); + + // TODO Show blinking cursor + write_strings(Commands.StartBlinking, Commands.BlinkingUnderlineShape); + + row, col := get_cursor_position(); + idx := 0; + + key := Keys.None; + + // TODO Some of these may be nice to have: + // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line + + while key != Keys.Resize && key != Keys.Escape { + // TODO How to alloc/release temporary memory here? + key = get_key(); + + if key == { + case Keys.Enter; + break; + + case Keys.Left; + if idx == 0 continue; + idx -= 1; + + case Keys.Right; + if idx == str.count-1 continue; + idx += 1; + + case Keys.Delete; + if idx == str.count-1 continue; + for idx..str.count-2 { + str.data[it] = str.data[it+1]; + } + + case Keys.Backspace; + if idx == 0 continue; + idx -= 1; + for idx..str.count-2 { + str.data[it] = str.data[it+1]; + } + + case; + if is_escape_code(key) continue; // TODO NOT WORKING FIX THIS + key_str := to_string(key); + str.data[idx] = key_str.data[0]; + idx += 1; + } + + + // append(*builder, str); + // set_cursor_position(row, col); + // write_builder(*builder, false); + set_cursor_position(row, col+idx); + for idx..count_limit print_character(#char " "); + set_cursor_position(row, col); + write_string(str); + // print(">%<", builder_to_string(*builder,, temporary_allocator)); + set_cursor_position(row, col+idx); + } + + write_strings(Commands.StopBlinking, Commands.DefaultShape); + /* + Use the get_key to read user input and show it on screen... should allow to move the cursor left and right and to delete/backspace. + Enter should end the input, returning the input string and the Enter key. + Escape should discard the input returning an empty string and a None key. + Resize should discard the input returning an empty string and a Resize key. + */ + // result := ifx key == Keys.Enter then builder_to_string(*builder) else ""; + result := ifx key == Keys.Enter then str else ""; + return result, key; +} + +start :: () { + if initialized == true return; + + input_string.data = input_buffer.data; + input_string.count = 0; + input_override = xx Keys.None; + + write_strings( + Commands.HideCursor, + Commands.SaveCursorPosition, + Commands.AlternateScreenBuffer, + Commands.SetUTF8, + Commands.CursorNormalMode, + Commands.KeypadNumMode); + + OS_prepare_terminal(); + + initialized = true; +} + +stop :: () { + if initialized == false return; + initialized = false; + + OS_reset_terminal(); + write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); +} + +flush_input :: () { + OS_flush_input(); + input_string.data = input_buffer.data; + input_string.count = 0; +} + +// TODO move style related procedures here! +// set_style :: () { + // print("", Commands.) -- +// } + +draw_box :: (x: int, y: int, width: int, height: int) { + assert_is_initialized(); + + // TODO Check if using a String_Builder improves performance (measure it)! + // TODO Validate input parameters against the terminal size. + assert(x > 0 && y > 0 && width > 1 && height > 1, "Invalid arguments."); + + auto_release_temp(); + + tmp_string: string; + + tmp_string = tprint(Commands.SetCursorPosition, y, x); + write_strings( + Commands.DrawingMode, + tmp_string, + Drawings.CornerTL + ); + + for 1..width-2 { + write_string(Drawings.LineH); + } + write_string(Drawings.CornerTR); + + for idx: y+1..y+height-2 { + tmpL := tprint(Commands.SetCursorPosition, idx, x); + tmpR := tprint(Commands.SetCursorPosition, idx, x+width-1); + write_strings( + tmpL, + Drawings.LineV, + tmpR, + Drawings.LineV); + } + + tmpBL := tprint(Commands.SetCursorPosition, y+height-1, x); + write_strings( + tmpBL, + Drawings.CornerBL); + for 1..width-2 { + write_string(Drawings.LineH); + } + write_string(Drawings.CornerBR); + + write_string(Commands.TextMode); +} + +// TODO Maybe rename to "clear()" +clear_terminal :: inline () { + assert_is_initialized(); + write_string(Commands.ClearScreen); +} + +// TODO Maybe rename to "get_size()" +get_terminal_size :: () -> rows: int, columns: int { + assert_is_initialized(); + + auto_release_temp(); + + flush_input(); + write_string(Commands.QueryWindowSizeInChars); + + rows, columns: int = ---; + if OS_wait_for_input(0) { + + // 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",, temporary_allocator); + + // Discard head noise. + while input.count >= 3 && (input[0] != FORMAT[0] || input[1] != FORMAT[1] || input[2] != FORMAT[2]) { + advance(*input); + } + + // Discard tail noise. + while input.count >= 3 && input[input.count-1] != FORMAT[FORMAT.count-1] { + input.count -= 1; + } + + assert(input.count >= 3 && + input[0] == FORMAT[0] && input[1] == FORMAT[1] && input[2] == FORMAT[2] && input[input.count-1] == FORMAT[FORMAT.count-1], + "Query window size in chars returned invalid response."); + + parts := split(input, ";",, temporary_allocator); + rows = parse_int(*parts[1]); + columns = parse_int(*parts[2]); + } + // Some systems don't allow to query the terminal size directly. + // In such cases, measure it indirectly by the maximum possible cursor position. + else { + flush_input(); + cursor_row, cursor_column := get_cursor_position(); + defer set_cursor_position(cursor_row, cursor_column); + + set_cursor_position(0xFFFF, 0xFFFF); + rows, columns = get_cursor_position(); + } + + return rows, columns; +} + +set_cursor_position :: (row: int, column: int) { + assert_is_initialized(); + auto_release_temp(); + print(Commands.SetCursorPosition, row, column); +} + +get_cursor_position :: () -> row: int, column: int { + assert_is_initialized(); + + 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"); + + // Discard head noise. + while input.count >= 2 && (input[0] != FORMAT[0] || input[1] != FORMAT[1]) { + advance(*input); + } + + // Discard tail noise. + while input.count >= 2 && input[input.count-1] != FORMAT[FORMAT.count-1] { + input.count -= 1; + } + + assert(input.count >= 2 && + input[0] == FORMAT[0] && input[1] == FORMAT[1] && input[input.count-1] == FORMAT[FORMAT.count-1], + "Query cursor position returned invalid response."); + + advance(*input, 2); + parts := split(input, ";",, temporary_allocator); + row := parse_int(*parts[0]); + column := parse_int(*parts[1]); + return row, column; +} + +set_terminal_title :: (title: string) { + assert_is_initialized(); + print(Commands.SetWindowTitle, title); +} + + +#if OS == .WINDOWS { + // Prototyping zone... keep clear! +} +else #if OS == .LINUX || OS == .MACOS { + // Prototyping zone... keep clear! +} diff --git a/modules/TUI/unix.jai b/modules/TUI/unix.jai new file mode 100644 index 0000000..861fe11 --- /dev/null +++ b/modules/TUI/unix.jai @@ -0,0 +1,286 @@ +#scope_file + +#import "Atomics"; +#import "System"; +#import "POSIX"; + + // Required to do unlocking input. + libc :: #system_library "libc"; + + // TODO Remote this. + // cfmakeraw :: (termios: *Terminal_IO_Mode) -> void #foreign libc; + /* https://elixir.bootlin.com/glibc/glibc-2.28/source/termios/cfmakeraw.c#L22 + void + cfmakeraw (struct termios *t) + { + t->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON); + t->c_oflag &= ~OPOST; + t->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN); + t->c_cflag &= ~(CSIZE|PARENB); + t->c_cflag |= CS8; + t->c_cc[VMIN] = 1; // read returns when one char is available. + t->c_cc[VTIME] = 0; + } + */ + + // 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; + tcsetattr :: (fd: s32, optional_actions: s32, termios_p : *Terminal_IO_Mode) -> s32 { + // TODO IMPLEMENT ME + + #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 SetAttributesActions.TCSANOW; + cmd = TCSETS; + + case xx SetAttributesActions.TCSADRAIN; + cmd = TCSETSW; + + case xx SetAttributesActions.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 #foreign libc; + // tcgetattr :: (fd: s32, termios_p: *Terminal_IO_Mode) -> s32 { + // TODO IMPLEMENT ME + // } + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcflush.c.html + // tcflush :: (fd: s32, queue_selector: s32) -> s32 #foreign libc; + tcflush :: inline (fd: s32, queue_selector: s32) -> s32 { + TCFLSH :: 0x540B; + return ioctl(fd, TCFLSH, queue_selector); + } + + // Information for the termios.h enums is platform dependent and was retrieved from: + // https://elixir.bootlin.com/glibc/latest/source/sysdeps/unix/sysv/linux/bits/termios.h + + // Set Attributes Actions. + SetAttributesActions :: enum u32 { + TCSANOW :: 0; // Change immediately. + TCSADRAIN :: 1; // Change when pending output is written. + TCSAFLUSH :: 2; // Flush pending input before changing. + } + + // Input modes. + Input_Modes :: enum_flags u32 { + IGNBRK :: 0000001; // Ignore break condition. + BRKINT :: 0000002; // Signal interrupt on break. + IGNPAR :: 0000004; // Ignore characters with parity errors. + PARMRK :: 0000010; // Mark parity and framing errors. + INPCK :: 0000020; // Enable input parity check. + ISTRIP :: 0000040; // Strip 8th bit off characters. + INLCR :: 0000100; // Map NL to CR on input. + IGNCR :: 0000200; // Ignore CR. + ICRNL :: 0000400; // Map CR to NL on input. + IUCLC :: 0001000; // Translate upper case input to lower case (not in POSIX). + IXON :: 0002000; // Enable start/stop output control. + IXANY :: 0004000; // Any character will restart after stop. + IXOFF :: 0010000; // Enable start/stop input control. + IMAXBEL :: 0020000; // Ring bell when input queue is full (not in POSIX). + IUTF8 :: 0040000; // Input is UTF8 (not in POSIX). + } + + // Output modes. + Output_Modes :: enum_flags u32 { + OPOST :: 0000001; // Perform output processing. + OLCUC :: 0000002; // Map lowercase characters to uppercase on output (not in POSIX). + ONLCR :: 0000004; // Map NL to CR-NL on output. + OCRNL :: 0000010; // Map CR to NL. + ONOCR :: 0000020; // Discard CR's when on column 0. + ONLRET :: 0000040; // Move to column 0 on NL. + OFILL :: 0000100; // Send fill characters for delays. + OFDEL :: 0000200; // Fill is DEL. + VTDLY :: 0040000; // Select vertical-tab delays: + VT0 :: 0000000; // Vertical-tab delay type 0. + VT1 :: 0040000; // Vertical-tab delay type 1. + } + + // Control modes. + Control_Modes :: enum u32 { + CS5 :: 0000000; // 5 bits per byte. + CS6 :: 0000020; // 6 bits per byte. + CS7 :: 0000040; // 7 bits per byte. + CS8 :: 0000060; // 8 bits per byte. + CSIZE :: 0000060; // Number of bits per byte (mask). + CSTOPB :: 0000100; // Two stop bits instead of one. + CREAD :: 0000200; // Enable receiver. + PARENB :: 0000400; // Parity enable. + PARODD :: 0001000; // Odd parity instead of even. + HUPCL :: 0002000; // Hang up on last close. + CLOCAL :: 0004000; + } + + // Local modes. + Local_Modes :: enum_flags u32 { + ISIG :: 0000001; // Enable signals. + ICANON :: 0000002; // Do erase and kill processing. + ECHO :: 0000010; // Enable echo. + ECHOE :: 0000020; // Visual erase for ERASE. + ECHOK :: 0000040; // Echo NL after KILL. + ECHONL :: 0000100; // Echo NL even if ECHO is off. + NOFLSH :: 0000200; // Disable flush after interrupt. + TOSTOP :: 0000400; // Send SIGTTOU for background output. + IEXTEN :: 0100000; // Enable DISCARD and LNEXT. + } + + // Control Characters + Control_Chars :: enum u8 { + VINTR :: 0; + VQUIT :: 1; + VERASE :: 2; + VKILL :: 3; + VEOF :: 4; + VTIME :: 5; // Time-out value (tenths of a second) [!ICANON]. + VMIN :: 6; // Minimum number of bytes read at once [!ICANON]. + VSWTC :: 7; + VSTART :: 8; + VSTOP :: 9; + VSUSP :: 10; + VEOL :: 11; + VREPRINT :: 12; + VDISCARD :: 13; + VWERASE :: 14; + VLNEXT :: 15; + VEOL2 :: 16; + } + + // The struct termios. + Terminal_IO_Mode :: struct { + 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 : [32]Control_Chars;// Control characters. + c_ispeed : u32; // Input speed (baud rates). + c_ospeed : u32; // Output speed (baud rates). + } + + initial_tio_mode: Terminal_IO_Mode; + raw_tio_mode: Terminal_IO_Mode; + +//////////////////////////////////////////////////////////////////////////////// +// Resize detection +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_export + +OS_prepare_terminal :: () { + tcgetattr(STDIN_FILENO, *initial_tio_mode); // TODO Log error using `log()` from jai/modules/Basic/Print.jai ? + 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; + tcsetattr(STDIN_FILENO, 0, *raw_tio_mode); // TODO Log on error. + + was_resized = false; + prepare_resize_handler(); +} + +OS_reset_terminal :: () { + restore_resize_handler(); + tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); // TODO Log on error. +} + +OS_flush_input :: inline () { + TCIFLUSH :: 0; // TODO Is this always zero in all systems? + tcflush(STDIN_FILENO, TCIFLUSH); +} + +// TODO Nothing is checking for the errors returned by this... shame! +OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bool = false, error_message: string = "" { + bytes_read := read(STDIN_FILENO, buffer, xx bytes_to_read); + if bytes_read < 0 { + error_code, error_message := get_error_value_and_string(); + return -1, true, error_message; + } + return bytes_read; +} + +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { + fds := pollfd.[ .{ fd = STDIN_FILENO, events = POLLIN, revents = 0 } ]; + nfds := fds.count; + poll_return := poll(fds.data, xx nfds, xx timeout_milliseconds); // TODO Wait for input using poll syscall. This breaks and throws '-1 | 4 | Interrupted system call' when we resize the window while polling. + error_code, error_message := get_error_value_and_string(); // FIXME Not used. + return ifx poll_return > 0 then true else false; +} + +// TODO This procedure hides the behaviour of reseting on read. +// We should have the `was_resized` on module.jai so that we know it may be used in another thread. +OS_was_terminal_resized :: () -> bool { + return atomic_swap(*was_resized, false); // TODO If the windows implementation is similar, we may push this into the main module file. +} diff --git a/modules/TUI/windows.jai b/modules/TUI/windows.jai new file mode 100644 index 0000000..f79a5cf --- /dev/null +++ b/modules/TUI/windows.jai @@ -0,0 +1,406 @@ +#scope_file + +#import "Basic"; +#import "Atomics"; +#import "System"; +#import "Windows"; + + // https://learn.microsoft.com/windows/win32/winprog/windows-data-types + LPVOID :: *void; + BOOL :: bool; + CHAR :: s8; + WCHAR :: s16; + SHORT :: s16; + USHORT :: u16; + WORD :: u16; + DWORD :: s32; + LPDWORD :: *s32; + UINT :: u32; + + + PINPUT_RECORD :: *INPUT_RECORD; + +// https://learn.microsoft.com/windows/console/console-virtual-terminal-sequences +// https://learn.microsoft.com/windows/console/console-virtual-terminal-sequences#designate-character-set +// https://github.com/MicrosoftDocs/Console-Docs/blob/main/docs/console-virtual-terminal-sequences.md + + kernel32 :: #system_library "kernel32"; + + // TODO Cleanup unused foreign procedures. + + // https://learn.microsoft.com/windows/console/getconsolescreenbufferinfo + GetConsoleScreenBufferInfo :: (hConsoleOutput: HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) -> bool #foreign kernel32; + + // https://learn.microsoft.com/en-us/windows/console/readconsoleinput + ReadConsoleInputA :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> success: bool #foreign kernel32; + + // https://learn.microsoft.com/windows/console/readconsole + ReadConsoleA :: (hConsoleInput: HANDLE, lpBuffer: LPVOID, nNumberOfCharsToRead: DWORD, lpNumberOfCharsRead: LPVOID, pInputControl := LPVOID) -> success: bool #foreign kernel32; + + // https://learn.microsoft.com/windows/console/getconsolemode + GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *DWORD) -> BOOL #foreign kernel32; + + // https://learn.microsoft.com/windows/console/setconsolemode + SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: DWORD) -> BOOL #foreign kernel32; + + // https://learn.microsoft.com/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror + GetLastError :: () -> s32 #foreign kernel32; + + // https://learn.microsoft.com/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject + WaitForSingleObject :: (hHandle: HANDLE, dwMilliseconds: DWORD) -> s32 #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 + PeekConsoleInputA :: (hConsoleInput: HANDLE, lpBuffer: PINPUT_RECORD, nLength: DWORD, lpNumberOfEventsRead: LPDWORD) -> bool #foreign kernel32; + + // 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 enable, control keys (Ctrl+C, Backspace, ...) are processed by the system. + ENABLE_LINE_INPUT; // If enable, ReadFile or ReadConsole function return on CR; otherwise they return when one or more characters are available. + ENABLE_ECHO_INPUT; // Echoes input on screen. Only available if ENABLE_LINE_INPUT is set. + ENABLE_WINDOW_INPUT; + ENABLE_MOUSE_INPUT; // + ENABLE_INSERT_MODE; // If enabled, text entered will be inserted at the current cursor location and all text following that location will not be overwritten. When disabled, all following text will be overwritten. + _UNUSED_0040_; + _UNUSED_0080_; + _UNUSED_0100_; + ENABLE_VIRTUAL_TERMINAL_INPUT; // + _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; // + ENABLE_WRAP_AT_EOL_OUTPUT; // + ENABLE_VIRTUAL_TERMINAL_PROCESSING; // + DISABLE_NEWLINE_AUTO_RETURN; // + ENABLE_LVB_GRID_WORLDWIDE; // + _UNUSED_0020_; + _UNUSED_0040_; + _UNUSED_0080_; + } + + COORD :: struct { + X : SHORT; + Y : SHORT; + } + + SMALL_RECT :: struct { + Left : SHORT; + Top : SHORT; + Right : SHORT; + Bottom : SHORT; + } + + CONSOLE_SCREEN_BUFFER_INFO :: struct { + dwSize : COORD; + dwCursorPosition : COORD; + wAttributes : WORD; + srWindow : SMALL_RECT; + dwMaximumWindowSize : COORD; + } + + 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; + } + + stdin: HANDLE; + initial_stdin_mode: u32; + raw_stdin_mode: Console_Input_Mode; + + stdout: HANDLE; + initial_stdout_mode: u32; + raw_stdout_mode: Console_Output_Mode; + + +//////////////////////////////////////////////////////////////////////////////// +// Resize detection +was_resized : bool; + +resize_handler :: (signal_code : s32) #c_call { + /* TODO + Try to implement using: + https://learn.microsoft.com/en-us/windows/console/reading-input-buffer-events + https://learn.microsoft.com/en-us/windows/console/readconsoleinput + https://learn.microsoft.com/en-us/windows/console/getnumberofconsoleinputevents + */ + new_context : Context; + push_context new_context { + if signal_code != SIGWINCH then return; + atomic_swap(*was_resized, true); + } +} + +prepare_resize_handler :: () { + /* TODO + sa : sigaction_t; + sa.sa_handler = resize_handler; + sigemptyset(*(sa.sa_mask)); + sa.sa_flags = SA_RESTART; + sigaction(SIGWINCH, *sa, null); + */ +} + +restore_resize_handler :: () { + /* TODO + sa : sigaction_t; + sa.sa_handler = SIG_DFL; + sigaction(SIGWINCH, null, *sa); + */ +} + +peek_input :: inline () -> INPUT_RECORD { + record: INPUT_RECORD; + records_read: s32; + if PeekConsoleInputA(stdin, *record, 1, *records_read) == false { + _, error_message := get_error_value_and_string(); + assert(false, error_message); + } + return record; +} + +read_input :: inline () -> INPUT_RECORD { + record: INPUT_RECORD; + records_read: s32; + if ReadConsoleInputA(stdin, *record, 1, *records_read) == false { + _, error_message := get_error_value_and_string(); + assert(false, error_message); + } + return record; +} + +count_input :: inline () -> s32 { + count: s32; + if GetNumberOfConsoleInputEvents(stdin, *count) == false { + _, error_message := get_error_value_and_string(); + assert(false, error_message); + } + return count; +} + + +//////////////////////////////////////////////////////////////////////////////// + +#scope_export + +OS_prepare_terminal :: () { + + // stdin + stdin = GetStdHandle(STD_INPUT_HANDLE); + if stdin == INVALID_HANDLE_VALUE { + print("Invalid input handler.", to_standard_error = true); + return; + } + if xx GetConsoleMode(stdin, *initial_stdin_mode) == false { + print("Failed to get input mode.", to_standard_error = true); + return; + } + 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 SetConsoleMode(stdin, xx raw_stdin_mode) == false { + print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); + return; + } + + // stdout + stdout = GetStdHandle(STD_OUTPUT_HANDLE); + if stdout == INVALID_HANDLE_VALUE { + print("Invalid output handler.", to_standard_error = true); + return; + } + if xx GetConsoleMode(stdout, *initial_stdout_mode) == false { + print("Failed to get output mode.", to_standard_error = true); + return; + } + 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 SetConsoleMode(stdout, xx raw_stdout_mode) == false { + print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); + return; + } +} + +OS_reset_terminal :: () { + if xx SetConsoleMode(stdin, initial_stdin_mode) == false { + print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); + return; + } + if xx SetConsoleMode(stdout, initial_stdout_mode) == false { + print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); + return; + } +} + +OS_flush_input :: inline () { + /* NOTE + 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. + */ + success := FlushConsoleInputBuffer(stdin); + if success == false { + _, error_message := get_error_value_and_string(); + assert(false, error_message); // TODO A bit harsh arent we? + } +} + +OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bool = false, error_message: string = "" { + + assert(bytes_to_read <= 0x7fff_ffff, "The Windows API only allows to read up to s32 bytes from the standard input."); + + bytes_read: s32 = 0; + available_inputs := count_input(); + + while bytes_to_read > 0 && available_inputs > 0 { + record := read_input(); + + if record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; + } + + if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true { + buffer[bytes_read] = xx record.KeyEvent.AsciiChar; + bytes_to_read -= 1; + bytes_read += 1; + } + available_inputs -= 1; + } + return bytes_read; +} + +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { + + /* TODO + Add a good comment explaining how the windows part of the module was implemented... what's the idea behind it. + Something like, Since windows provides a single input buffer with all events, we need to peek through them and + discard the ones that are of no use for us. + Because it's a single buffer, all functions need to do repeated work (see if it's resize, see if it's a key press) + ... and so on. + */ + + expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0); + + // Possible values for poll_return TODO NOT BEING USED + WAIT_ABANDONED :: 0x00000080; // Mutex stuff. + WAIT_OBJECT_0 :: 0x00000000; // Detected input. + WAIT_TIMEOUT :: 0x00000102; // Reached timeout. + WAIT_FAILED :: 0xFFFFFFFF; // Something went wrong. + + while true { + poll_return := WaitForSingleObject(stdin, timeout_milliseconds); + + // TODO Weird looking code... + if poll_return == { + case WAIT_TIMEOUT; + return false; + case WAIT_ABANDONED; + print("MUTEX STUFF"); // https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject + return false; + case WAIT_FAILED; + _, error_message := get_error_value_and_string(); + assert(false, error_message); + } + + // 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 { + was_resized = true; + read_input(); + } + 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 return false; + timeout_milliseconds = xx to_milliseconds(expiration - now); + } + + return false; +} + +OS_was_terminal_resized :: () -> bool { + while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; + read_input(); + } + + defer was_resized = false; + return was_resized; +} |
