diff options
| -rw-r--r-- | TUI/module.jai | 170 | ||||
| -rw-r--r-- | TUI/unix.jai | 141 | ||||
| -rw-r--r-- | TUI/windows.jai | 57 | ||||
| -rw-r--r-- | ttt.jai | 6 |
4 files changed, 195 insertions, 179 deletions
diff --git a/TUI/module.jai b/TUI/module.jai index 3298e59..ec319d7 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -9,13 +9,6 @@ #import "Basic"; #import "String"; -// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences -// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#designate-character-set -// https://github.com/MicrosoftDocs/Console-Docs/blob/main/docs/console-virtual-terminal-sequences.md - -isTUIActive := false; // TODO Rename this variable. - - Drawings :: struct { CornerBR :: "\x6A"; CornerTR :: "\x6B"; @@ -86,16 +79,20 @@ Commands :: struct { } -// TODO Maybe rename to "setup()" + +initialized := false; + + start :: () { + if initialized return; write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); OS_prepare_terminal(); - isTUIActive = true; + initialized = true; } -// TODO Maybe rename to "reset()" stop :: () { - isTUIActive = false; + if initialized == false return; + initialized = false; OS_reset_terminal(); write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } @@ -146,19 +143,17 @@ draw_box :: (x: int, y: int, width: int, height: int) { // TODO Maybe rename to "clear()" clear_terminal :: inline () { + assert(initialized, "TUI is not ready."); write_string(Commands.ClearScreen); } // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { + assert(initialized, "TUI is not ready."); rows, columns := OS_get_terminal_size(); return rows, columns; } -// read_input: () -> string { - // return OS_read_input(); -// } - set_cursor_position :: (row: int, column: int) { auto_release_temp(); tmp_string := tprint(Commands.SetCursorPosition, row, column); @@ -166,21 +161,24 @@ set_cursor_position :: (row: int, column: int) { } get_cursor_position :: () -> row: int, column: int { - assert(isTUIActive, "TUI is not active."); // TODO + assert(initialized, "TUI is not ready."); // TODO Should I use this inside each and every procedure? + + auto_release_temp(); + + write_string(Commands.QueryCursorPosition); - // TODO Hide input echo. + input := talloc_string(64); + input.count = OS_read_input(input.data, input.count); // TODO Does not check for read errors. - write_string(Commands.QueryCursorPosition); // Returned - input := read_input(.MACHINE); - // Result: \e[21;1R + // Expected message format: \e[<r>;<c>R + // where <r> is the number of rows and <c> of columns. assert( - input.data[0] == #char "\e" && - input.data[1] == #char "[" && - input.data[input.count-1] == #char "R", + input[0] == #char "\e" && + input[1] == #char "[" && + input[input.count-1] == #char "R", "Query cursor position returned invalid response."); - input.data += 2; - print(">%<\n", input); + advance(*input, 2); parts := split(input, ";"); row := parse_int(*parts[0]); column := parse_int(*parts[1]); @@ -192,12 +190,33 @@ Input_Mode :: enum u8 { MACHINE; // Hides cursor, hides input, and reads right away once the first input is available. } -read_input :: (mode: Input_Mode = .HUMAN) -> string { - if mode == .HUMAN write_string(Commands.ShowCursor); - defer if mode == .HUMAN write_string(Commands.HideCursor); - return OS_read_input(mode); -} +read_input :: (allocator: Allocator = temp, $mode: Input_Mode = .HUMAN) -> string { + #if mode == .HUMAN { + write_string(Commands.ShowCursor); + defer write_string(Commands.HideCursor); + + OS_set_input_mode(.HUMAN); + defer OS_set_input_mode(.MACHINE); + } + assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); + + #assert(mode != .MACHINE); // TODO Keep an eye if I try to use read_input for machine read. Eventually, remove mode from the procedure arguments. + + builder: String_Builder(); + builder.allocator = allocator; + init_string_builder(*builder); + + while(1) { + buffer := get_current_buffer(*builder); + buffer_data := get_buffer_data(buffer); + buffer.count = OS_read_input(buffer_data, buffer.allocated); // TODO Does not check for read errors. + if buffer.count == 0 || buffer_data[buffer.count-1] == #char "\n" break; + assert(buffer.count == buffer.allocated); // TODO If newline wasn't detected, it's because the buffer got full. + expand(*builder); + } + return builder_to_string(*builder, allocator); +} #if OS == .WINDOWS { @@ -205,95 +224,4 @@ read_input :: (mode: Input_Mode = .HUMAN) -> string { } else #if OS == .LINUX || OS == .MACOS { // Prototyping zone... keep clear! - OS_read_input :: (mode: Input_Mode) -> string { - - term : My_Termios; - tcgetattr(STDIN_FILENO, *term); - - // Input modes. - Input_Modes :: enum_flags u32 { - IGNBRK; // Ignore break condition. - BRKINT; // Signal interrupt on break. - IGNPAR; // Ignore characters with parity errors. - PARMRK; // Mark parity and framing errors. - INPCK; // Enable input parity check. - ISTRIP; // Strip 8th bit off characters. - INLCR; // Map NL to CR on input. - IGNCR; // Ignore CR. - ICRNL; // Map CR to NL on input. - IXON; // Enable start/stop output control. - IXOFF; // Enable start/stop input control. - IXANY; // Any character will restart after stop. - __NOT_USED__; - IMAXBEL; // Ring bell when input queue is full. - IUCLC; // Translate upper case input to lower case. - } - - // Local modes. - Local_Modes :: enum_flags u32 { - ECHOKE; // Visual erase for KILL. - ECHOE; // Visual erase for ERASE. - ECHOK; // Echo NL after KILL. - ECHO; // Enable echo. - ECHONL; // Echo NL even if ECHO is off. - ECHOPRT; // Hardcopy visual erase. - ECHOCTL; // Echo control characters as ^X. - ISIG; // Enable signals. - ICANON; // Do erase and kill processing. - ALTWERASE; // Alternate WERASE algorithm. - IEXTEN; // Enable DISCARD and LNEXT. - EXTPROC; // External processing. - TOSTOP; // Send SIGTTOU for background output. - FLUSHO; // Output being flushed (state). - XCASE; // Canonical upper/lower case. - NOKERNINFO; // Disable VSTATUS. - PENDIN; // Retype pending input (state). - NOFLSH; // Disable flush after interrupt. - - } - - backup: My_Termios; - tcgetattr(STDIN_FILENO, *backup); - - if mode == { - case .HUMAN; - term.c_iflag |= xx cast(Input_Modes)(.IXOFF | .ICRNL); - term.c_lflag |= xx cast(Local_Modes)(.NOKERNINFO | .ECHO | .ECHOE | .ECHOKE); - - case .MACHINE; - iflags: Input_Modes = (.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); - term.c_iflag &= xx ~(iflags); - lflags: Local_Modes = (.ECHO | .ECHONL | .ICANON | .IEXTEN); - term.c_lflag &= xx ~lflags; - } - - // term.c_iflag &= 0xFFFFFA14;// ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); - // term.c_oflag &= 0xFFFFFFFE;// ~OPOST; - // term.c_lflag &= 0xFFFF7FB4;// ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); - // term.c_cflag &= 0xFFFFFECF;// ~(CSIZE | PARENB); - // term.c_cflag |= 0x00000030; - - tcsetattr(STDIN_FILENO, 0, *term); - - result: string = ---; - buffer: [8192] u8; - write_string(Commands.ShowCursor); - bytes_read := read(STDIN_FILENO, buffer.data, buffer.count-1); - write_string(Commands.HideCursor); - - // TODO WIP WIP WIP - result.data = alloc(bytes_read, temp); - memcpy(result.data, buffer.data, bytes_read); - result.count = bytes_read; - // result = "jat"; - // result.count = bytes_read; - // result.data = buffer.data; - // result = copy_temporary_string(buffer); - // result = tprint("%", xx buffer.data); - // result = to_string(buffer.data, bytes_read); // TODO WIP WIP WIP This is still using the stack allocated buffer and WILL FAIL! - - tcsetattr(STDIN_FILENO, 0, *backup); - - return result; - }; } diff --git a/TUI/unix.jai b/TUI/unix.jai index e5c81c3..6bcf13a 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -1,9 +1,61 @@ +#scope_file + #import "POSIX"; +#import "System"; + + // Required to do unlocking input. + 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; - __term : My_Termios; + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html + tcgetattr :: (fd : s32, termios_p : *Terminal_IO_Mode) -> s32 #foreign libc; - My_Termios :: struct { + + // Input modes. + Input_Modes :: enum_flags u32 { + IGNBRK; // Ignore break condition. + BRKINT; // Signal interrupt on break. + IGNPAR; // Ignore characters with parity errors. + PARMRK; // Mark parity and framing errors. + INPCK; // Enable input parity check. + ISTRIP; // Strip 8th bit off characters. + INLCR; // Map NL to CR on input. + IGNCR; // Ignore CR. + ICRNL; // Map CR to NL on input. + IXON; // Enable start/stop output control. + IXOFF; // Enable start/stop input control. + IXANY; // Any character will restart after stop. + __NOT_USED__; + IMAXBEL; // Ring bell when input queue is full. + IUCLC; // Translate upper case input to lower case. + } + + // Local modes. + Local_Modes :: enum_flags u32 { + ECHOKE; // Visual erase for KILL. + ECHOE; // Visual erase for ERASE. + ECHOK; // Echo NL after KILL. + ECHO; // Enable echo. + ECHONL; // Echo NL even if ECHO is off. + ECHOPRT; // Hardcopy visual erase. + ECHOCTL; // Echo control characters as ^X. + ISIG; // Enable signals. + ICANON; // Do erase and kill processing. + ALTWERASE; // Alternate WERASE algorithm. + IEXTEN; // Enable DISCARD and LNEXT. + EXTPROC; // External processing. + TOSTOP; // Send SIGTTOU for background output. + FLUSHO; // Output being flushed (state). + XCASE; // Canonical upper/lower case. + NOKERNINFO; // Disable VSTATUS. + PENDIN; // Retype pending input (state). + NOFLSH; // Disable flush after interrupt. + + } + + Terminal_IO_Mode :: struct { c_iflag : u32; // Input mode flags. c_oflag : u32; // Output mode flags. c_cflag : u32; // Control modes flags. @@ -14,50 +66,85 @@ c_ospeed : u32; // Output speed (baud rates). } - // Required to do unlocking input. - libc :: #system_library "libc"; - // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcsetattr.c.html - tcsetattr :: (fd : s32, optional_actions : s32, termios_p : *My_Termios) -> s32 #foreign libc; + initial_tio_mode : Terminal_IO_Mode; + default_tio_mode: Terminal_IO_Mode; + blocking_tio_mode: Terminal_IO_Mode; + unblocking_tio_mode: Terminal_IO_Mode; - // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html - tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; - + +#scope_export OS_prepare_terminal :: () { // TODO Required to do unlocking input. - tcgetattr(STDIN_FILENO, *__term); - term_new := __term; + tcgetattr(STDIN_FILENO, *initial_tio_mode); + default_tio_mode := initial_tio_mode; + + default_tio_mode.c_iflag &= 0xFFFFFA14;// ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); + default_tio_mode.c_oflag &= 0xFFFFFFFE;// ~OPOST; + default_tio_mode.c_lflag &= 0xFFFF7FB4;// ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + default_tio_mode.c_cflag &= 0xFFFFFECF;// ~(CSIZE | PARENB); + default_tio_mode.c_cflag |= 0x00000030; // TODO WHAT IS THIS? - term_new.c_iflag &= 0xFFFFFA14;// ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); - term_new.c_oflag &= 0xFFFFFFFE;// ~OPOST; - term_new.c_lflag &= 0xFFFF7FB4;// ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); - term_new.c_cflag &= 0xFFFFFECF;// ~(CSIZE | PARENB); - term_new.c_cflag |= 0x00000030; // TODO WHAT IS THIS? + blocking_tio_mode = default_tio_mode; + blocking_tio_mode.c_iflag |= xx cast(Input_Modes)(.IXOFF | .ICRNL); + blocking_tio_mode.c_lflag |= xx cast(Local_Modes)(.NOKERNINFO | .ECHO | .ECHOE | .ECHOKE); - tcsetattr(STDIN_FILENO, 0, *term_new); + unblocking_tio_mode = default_tio_mode; + iflags: Input_Modes = (.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); + unblocking_tio_mode.c_iflag &= xx ~(iflags); + lflags: Local_Modes = (.ECHO | .ECHONL | .ICANON | .IEXTEN); + unblocking_tio_mode.c_lflag &= xx ~lflags; + + tcsetattr(STDIN_FILENO, 0, *default_tio_mode); } OS_reset_terminal :: () { - tcsetattr(STDIN_FILENO, 0, *__term); // return echo + tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); // return echo } OS_get_terminal_size :: () -> rows: int, columns: int { - buffer: [512] u8; - write_string(Commands.QueryWindowSizeInChars); - bytes_read := read(STDIN_FILENO, buffer.data, buffer.count-1); - str := to_string(buffer.data, bytes_read); + auto_release_temp(); + + input := talloc_string(64); + write_string(Commands.QueryWindowSizeInChars); + input.count = OS_read_input(input.data, input.count); - // Result: [8;79;156t + // Expected message format: [8;<r>;<c>t + // where <r> is the number of rows and <c> of columns. assert( - buffer.data[0] == #char "\e" && - buffer.data[1] == #char "[" && - buffer.data[2] == #char "8", + input[0] == #char "\e" && + input[1] == #char "[" && + input[2] == #char "8", "Query windows size in chars returned invalid response."); - parts := split(str, ";"); + parts := split(input, ";"); rows := parse_int(*parts[1]); columns := parse_int(*parts[2]); return rows, columns; } + +// TODO Maybe we should use a NON-BLOCKING state by default... and only change to blocking when performing a HUMAN read...? + +OS_set_input_mode :: (mode: Input_Mode) { + if mode == { + case .HUMAN; + tcsetattr(STDIN_FILENO, 0, *blocking_tio_mode); + // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) + case .MACHINE; + tcsetattr(STDIN_FILENO, 0, *unblocking_tio_mode); + // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) + case; + // TODO ERROR + } +} + +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; +} diff --git a/TUI/windows.jai b/TUI/windows.jai index 84dd271..85dab0b 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -1,4 +1,12 @@ +#scope_file + #import "Windows"; +#import "System"; + +// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences +// https://learn.microsoft.com/en-us/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"; @@ -84,7 +92,9 @@ initial_stdout_mode: u32; default_stdout_mode: Console_Mode; - + +#scope_export + OS_prepare_terminal :: () { print("TODO TUI\n", to_standard_error = true); @@ -147,38 +157,25 @@ OS_get_terminal_size :: () -> rows: int, columns: int { return rows, columns; } -OS_read_input :: (mode: Input_Mode) -> string { - result: string = ---; - +OS_set_input_mode :: (mode: Input_Mode) { if mode == { case .HUMAN; - assert(SetConsoleMode(stdin, xx blocking_stdin_mode), "Failed to set input mode to blocking mode."); // @speed TODO Could check if was already set before applying. - + assert(SetConsoleMode(stdin, xx blocking_stdin_mode), "Failed to set input mode to blocking mode."); + // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) case .MACHINE; - // assert(SetConsoleMode(stdin, xx unblocking_stdin_mode), "Failed to set input mode to unblocking mode."); // @speed TODO Could check if was already set before applying. - if SetConsoleMode(stdin, xx unblocking_stdin_mode) == false { - print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); - exit(0); - } + assert(SetConsoleMode(stdin, xx unblocking_stdin_mode), "Failed to set input mode to unblocking mode."); + // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) + case; + // TODO ERROR } +} - MAX_BYTES_TO_READ :: 10; - temp : [MAX_BYTES_TO_READ] u8; - bytes_read : s32; - - if ReadConsoleA(stdin, temp.data, xx temp.count, *bytes_read) { - - // TODO If the number of bytes_read is equal to the buffer size... we may have some more data to read?! - // ---> To fix this, we should check the last read byte and see if it's a newline (at least for HUMAN mode)! - - // TODO Maybe pass the input result in the temporary memory (unless specified otherwise)?! - - result.data = alloc(bytes_read); - result.count = bytes_read; - memcpy(result.data, temp.data, bytes_read); +OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bool = false, error_message: string = "" { + bytes_read: s32; + error := ReadConsoleA(stdin, buffer, bytes_to_read, *bytes_read); + if error { + _, error_message := get_error_value_and_string(); + return -1, true, error_message; } - - assert(SetConsoleMode(stdin, xx default_stdin_mode), "Failed to set default input mode."); // @speed TODO Maybe compare current mode with default one before applying?! - - return result; -}; + return bytes_read; +} @@ -1191,7 +1191,7 @@ main :: () { TUI.set_cursor_position(3, 3); input := TUI.read_input(); - // sleep_milliseconds(1500); + // sleep_milliseconds(5000); TUI.stop(); print("- - -\n"); print("window r:c = %:%\n", rows, columns); @@ -1251,6 +1251,10 @@ main :: () { 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); + + // TODO app data should be stored under: + // Windows: APPDATA (~/AppData/Roaming) + // Unix: XDG_DATA_HOME (~/.local/share) make_directory_if_it_does_not_exist(app_directory, recursive = true); } |
