From c6a0b31e52d6b90566c7a876ce42b03abd4b9d70 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 24 Jan 2023 22:10:28 +0000 Subject: Lets try out jai. --- ttt.jai | 1873 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1873 insertions(+) create mode 100644 ttt.jai (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai new file mode 100644 index 0000000..a2a4e8f --- /dev/null +++ b/ttt.jai @@ -0,0 +1,1873 @@ +// Copyright 2022 Daniel Martins +// License GPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + + +// Compilation commands: +// - debug : jai ttt.jai -exe ttt +// - release : jai ttt.jai -exe ttt -x64 -release +// +// Compiler flags: +// -l : libraries to link +// -o : output file name +// -Wall : enables all compiler's warning messages +// -Werror : make all warnings into errors +// -pedantic : issue all the warnings demanded by strict ISO C +// -O : code optimization level (commonly accepted as best: 2) +// -g : debug information level (max: 3) +// -m64 : 64b architecture +// -D : defines for preprocessor +// -static-pie : link statically producing an position-independent executable +// -DNDEBUG : remove assertions from code (not used) + +/* +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +*/ + +// #define VERSION "2.0" // Use only 3 chars (to fit layouts). +VERSION :: "2.0"; +// #define TASK_NAME_LENGTH 57 // Task name length. +TASK_NAME_LENGTH :: 57; +// #define TASK_NAME_BYTES (TASK_NAME_LENGTH+1) +TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; +// #define FIRST_DAY_OF_WEEK 1 // (0-6, Sunday = 0). +FIRST_DAY_OF_WEEK :: 1; +// #define NUM_WEEK_DAYS 7 // Just to be more clear about what we're looping about. +NUM_WEEK_DAYS :: 7; + +/* +#if defined(_WIN64) +#define HOME_PATH_ENV "USERPROFILE" +#else +#define HOME_PATH_ENV "HOME" +#endif +#define APP_FOLDER_NAME ".task_time_tracker" +#define DB_FILE_NAME "database.bin" +#define AR_FILE_NAME "archive.csv" + +typedef struct { + int64_t times[NUM_WEEK_DAYS]; + char name[TASK_NAME_BYTES]; +} task_st; + +typedef struct { + task_st *tasks; + size_t count; // Will always be equal or less than capacity. + size_t capacity; // Will always be equal or less than PTRDIFF_MAX (see MAX_DATABASE_TASKS). + ptrdiff_t active_task; // Will always be less than capacity/count. + ptrdiff_t selected_task; // Will always be less than capacity/count. + int64_t modified_on; + int64_t total_times[NUM_WEEK_DAYS]; +} database_st; + +#define DB_FILE_SIGN_STR "TTT:B:01" +const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; +const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; +const size_t SIZEOF_TASK_ST = sizeof(task_st); +const size_t SIZEOF_DATABASE_ST = sizeof(database_st); +const size_t SIZEOF_CHAR = sizeof(char); +const size_t SIZEOF_INT64 = sizeof(int64_t); +const int64_t SECONDS_IN_MINUTE = (int64_t)60; +const int64_t SECONDS_IN_HOUR = (int64_t)60*SECONDS_IN_MINUTE; +const int64_t SECONDS_IN_DAY = (int64_t)24*SECONDS_IN_HOUR; +const int64_t SECONDS_IN_YEAR = (int64_t)365*SECONDS_IN_DAY; +const size_t MAX_DATABASE_TASKS = (PTRDIFF_MAX < (SIZE_MAX / SIZEOF_TASK_ST)) ? PTRDIFF_MAX : (SIZE_MAX / SIZEOF_TASK_ST); + + +database_st database = { .tasks = NULL }; +database_st archive = { .tasks = NULL }; +database_st *db = NULL; +bool is_autosave_enabled = true; +int countdown_to_autosave = -1; +char *app_folder = NULL; +char *db_file_path = NULL; +char *ar_file_path = NULL; +char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors. +size_t string_buffer_size = 0; +int size_x, size_y, pos_x, pos_y; + + + +typedef enum { + STYLE_SELECTED = 1, + STYLE_SELECTED_INVERTED, + STYLE_ACTIVE, + STYLE_ACTIVE_SELECTED, + STYLE_ERROR, +} styles_et; + + + +WINDOW *error_window = NULL; +time_t error_time_limit = 0; + +void print_error(const char *format, ...) { + va_list args; + va_start(args, format); + if (stdscr == NULL || isendwin() == true) { // Not in ncurses mode? + vfprintf(stderr, format, args); + fprintf(stderr, "\n"); + } + else { + int w_size_x = size_x > 120 ? 120 : size_x - 2; + int w_size_y = 4; + if (error_window == NULL) { + error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); + wattron(error_window, COLOR_PAIR(STYLE_ERROR)); + wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); + mvwprintw(error_window, 0, 1, " Error "); + wmove(error_window, 1, 0); + } + else { + waddch(error_window, ' '); + } + vw_printw(error_window, format, args); + error_time_limit = time(NULL) + 5; + } + + va_end(args); +} + +void draw_error_window() { + if (error_window == NULL) { + return; + } + + // Hide error window after time-limit or if terminal is shrank. + int w_size_x, w_size_y; + getmaxyx(error_window, w_size_y, w_size_x); + if (time(NULL) >= error_time_limit + || size_x - w_size_x < 2 + || size_y - w_size_y < 2 + ) { + delwin(error_window); + error_window = NULL; + return; + } + + // Adjust error window position. + int pos_x = (size_x - w_size_x) / 2; + int pos_y = (size_y - w_size_y) / 2; + mvwin(error_window, pos_y, pos_x); + + // Avoid being overwritten by main window content. + refresh(); + touchwin(error_window); + wrefresh(error_window); +} + +void trigger_autosave() { + countdown_to_autosave = 13375; // ms +} + +void show_processing() { + mvaddch(0, 0, ACS_DIAMOND); + refresh(); +} + +// Checks if file is exists and is accessible. +// Returns true when the file exists and is accessible. +bool is_file_accessible(const char *path) { + assert(path != NULL); + FILE *file = fopen(path, "r+"); + bool is_file_accessible = file != NULL; + if (is_file_accessible) { + fclose(file); + } + return is_file_accessible; +} + +// Returns true if string to_compare is equal to any of the other passed strings, false otherwise. +bool is_equal_to_any(const char *to_compare, const char *test_a, const char *test_b) { + return strncmp(to_compare, test_a, strlen(test_a)+1) == 0 + || strncmp(to_compare, test_b, strlen(test_b)+1) == 0; +} + +// Given an UTF8 encoded string, truncate it to length bytes without breaking any UTF8 character. +// The string should have capacity for at least length + 1. +// The terminating null byte ('\0') is not included in length. +// Returns the truncated string length. +size_t truncate_string_utf8(char *string, size_t length) { + assert(string != NULL); + + // Find index of first continuation byte. + size_t idx = length; + while (idx > 0 && ((string[idx - 1] & 0xC0) == 0x80)) { + idx--; + } + size_t 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 && (string[idx - 1] & 0x80) == 0x00) + && !(continuation_bytes == 1 && (string[idx - 1] & 0xE0) == 0xC0) + && !(continuation_bytes == 2 && (string[idx - 1] & 0xF0) == 0xE0) + && !(continuation_bytes == 3 && (string[idx - 1] & 0xF8) == 0xF0) + ) { + length -= (continuation_bytes + 1); // Remove '+ 1' start byte. + } + + string[length] = '\0'; + return length; +} + +// Returns true when the string is empty or consists of white space characters. +bool is_empty_string(const char *string) { + for (int idx = 0; string[idx] != '\0'; idx++) { + switch(string[idx]) { + case ' ': + case '\t': + case '\v': + case '\f': + case '\r': + case '\n': + break; + default: + return false; + } + } + return true; +} + +// Uses strchr to replace all instances of find by replace. +// Returns string. +char *replace_char(char *string, char find, char replace) { + char *idx = string; + while((idx = strchr(idx, find)) != NULL) { + *idx = replace; + idx++; + } + return string; +} + +// Prints, on row y and column x, the time using 5 characters centered on space. +// Returns the result of a call to mvprintw. +int mvprintw_time(int y, int x, intmax_t time, int space) { + const int TIME_CHARS = 5; + assert(space >= TIME_CHARS); + + int left_padding = (space - TIME_CHARS) / 2; + int right_padding = space - TIME_CHARS - left_padding; + + if (time < 0) { + return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, ""); + } + else if (time == 0) { + return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, ""); + } + else if (time < SECONDS_IN_MINUTE) { + return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); + } + else if (time < (intmax_t)100 * SECONDS_IN_HOUR) { + intmax_t hours = (double)time / (double)SECONDS_IN_HOUR; + intmax_t minutes = (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE; + return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); + } + else if (time < (intmax_t)(9999.5 * SECONDS_IN_DAY)) { + double value = (double)time / (double)SECONDS_IN_DAY; + int decimals = + time >= 99.95 * SECONDS_IN_DAY ? 0 : + time >= 9.995 * SECONDS_IN_DAY ? 1 : + 2; + return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); + } + else if (time < (intmax_t)(9999.5 * SECONDS_IN_YEAR)) { + double value = (double)time / (double)SECONDS_IN_YEAR; + int decimals = + time >= 99.95 * SECONDS_IN_YEAR ? 0 : + time >= 9.995 * SECONDS_IN_YEAR ? 1 : + 2; + return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); + } + else { + return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); + } +} + +int64_t add_int64(int64_t x, int64_t y) { + int64_t result; +#ifdef __GNUC__ + bool overflow = __builtin_add_overflow(x, y, &result); + if (overflow) { + result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN) + } +#else + result = + (y > 0 && x > INT64_MAX - y) ? INT64_MAX : + (y < 0 && x < INT64_MIN - y) ? INT64_MIN : + x + y; +#endif + return result; +} + +int64_t sub_int64(int64_t x, int64_t y) { + int64_t result; +#ifdef __GNUC__ + bool overflow = __builtin_sub_overflow(x, y, &result); + if (overflow) { + result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN) + } +#else + result = + (y < 0 && x > INT64_MAX + y) ? INT64_MAX : + (y > 0 && x < INT64_MIN + y) ? INT64_MIN : + x - y; +#endif + return result; +} + +// Returns active task or NULL if none applies. +task_st *get_active_task(database_st *db) { + assert(db != NULL); + + task_st *task = NULL; + if (db->active_task >= 0) { + task = db->tasks + db->active_task; + } + return task; +} + +// Returns selected task or NULL if none applies. +task_st *get_selected_task(database_st *db) { + assert(db != NULL); + + task_st *task = NULL; + if (db->selected_task >= 0) { + task = db->tasks + db->selected_task; + } + return task; +} + +// Creates new task stored at location given by task pointer. +// If necessary, expands database capacity. +// Returns success. +bool create_task(database_st *db, task_st **task) { + assert(db != NULL); + assert(task != NULL); + + if (db->count >= MAX_DATABASE_TASKS) { + print_error("Database reached maximum capacity."); + return false; + } + + // If necessary, expand database capacity. + size_t current_capacity = db->capacity; + if ((db->count + 1) > current_capacity) { + size_t new_capacity = + current_capacity == 0 ? 2 : + current_capacity > (MAX_DATABASE_TASKS >> 1) ? MAX_DATABASE_TASKS : + current_capacity << 1; + + task_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST); + if (new_tasks == NULL) { + print_error("Failed to expand database."); + return false; + } + + db->capacity = new_capacity; + db->tasks = new_tasks; + } + + // Prepare new task. + *task = &db->tasks[db->count]; + memset(*task, 0, SIZEOF_TASK_ST); + + db->count++; + + // Adjust selected task. + if (db->selected_task < 0) { + db->selected_task = db->count-1; + } + + return true; +} + +// Duplicates the given task. Duplicated task is appended to the database. +// Returns success. +bool duplicate_task(database_st *db, task_st *task) { + assert(db != NULL); + assert(task != NULL); + + // Create new task and keep task_idx (relative pointer) of original task). + ptrdiff_t task_idx = task - db->tasks; + task_st *new_task; + if (create_task(db, &new_task) == false) { + return false; + } + + // If original task belonged to database, fix its pointer. + if (0 <= task_idx && task_idx < db->count - 1) { // Compensate '- 1' for the new task. + task = db->tasks + task_idx; + } + + memcpy(new_task, task, SIZEOF_TASK_ST); + + // Add task time values to total times. + for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + db->total_times[idx] = add_int64(db->total_times[idx], new_task->times[idx]); + } + + return true; +} + +// Deletes task from database. +// If possible, shrinks the database capacity. +// Returns success. +bool delete_task(database_st *db, task_st *task) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task - db->tasks < db->count); + + // Remove task timer values from total timers. + for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + db->total_times[idx] = sub_int64(db->total_times[idx], task->times[idx]); + } + + // Move tasks after the index position to their new positions. + ptrdiff_t index = task - db->tasks; + memmove(task, task + 1, (db->count - index - 1) * SIZEOF_TASK_ST); + db->count--; + + // Adjust selected task. + if (db->selected_task >= db->count) { + db->selected_task--; + } + + // Adjust active task. + if (db->active_task > index) { + db->active_task--; + } + else if (db->active_task == index) { + db->active_task = -1; + } + + // If possible, shrink database capacity. + size_t current_capacity = db->capacity; + if (db->count <= (current_capacity >> 2)) { + size_t new_capacity = current_capacity >> 1; + task_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST); + if (new_tasks == NULL && new_capacity > 0) { + print_error("Failed to shrink database."); + return false; + } + db->capacity = new_capacity; + db->tasks = new_tasks; + } + + return true; +} + +// Moves task to index. +// Index gets clamped to [0, db->count[. +void move_task_to_index(database_st *db, task_st *task, ptrdiff_t index) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task - db->tasks < db->count); + + ptrdiff_t target_index = index < 0 ? 0 + : index >= db->count ? db->count - 1 + : index; + + task_st *target_task = db->tasks + target_index; + + if (target_task == task) { + return; + } + + // Move task to new location. + task_st temp_task; + memcpy(&temp_task, task, SIZEOF_TASK_ST); + if (target_task > task) { + memmove(task, task + 1, (target_task - task) * SIZEOF_TASK_ST); + } + else { + memmove(target_task + 1, target_task, (task - target_task) * SIZEOF_TASK_ST); + } + memcpy(target_task, &temp_task, SIZEOF_TASK_ST); + + // Adjust active and selected tasks. + ptrdiff_t source_index = task - db->tasks; + if (db->active_task == source_index) { + db->active_task = target_index; + } + else if (source_index < db->active_task && db->active_task <= target_index) { + db->active_task--; + } + else if (target_index <= db->active_task && db->active_task < source_index) { + db->active_task++; + } + db->selected_task = target_index; +} + +// Updates the times on the active task (and adjusts database totals). +void update_times(database_st *db) { + assert(db != NULL); + + // Get current UTC time. + time_t stop_time = time(NULL); + + // Get last modified on UTC time. + time_t start_time = db->modified_on; + + // Keep track of this update. + db->modified_on = stop_time; + + if (db->active_task < 0) { + return; + } + + task_st *active_task = db->tasks + db->active_task; + uint8_t start_week_day; + while (start_time < stop_time) { + + start_week_day = localtime(&start_time)->tm_wday; + + // Get next day in local time. + struct tm *start_of_day_tm = localtime(&start_time); + start_of_day_tm->tm_sec = 0; + start_of_day_tm->tm_min = 0; + start_of_day_tm->tm_hour = 0; + time_t start_of_day = mktime(start_of_day_tm); + time_t next_day = start_of_day + SECONDS_IN_DAY; + time_t next_start = next_day < stop_time ? next_day : stop_time; + time_t elapsed_time = 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. +void update_total_times(database_st *db) { + assert(db != NULL); + + int64_t *totals = db->total_times; + memset(totals, 0, NUM_WEEK_DAYS * SIZEOF_INT64); + for (size_t idx = 0; idx < db->count; idx++) { + int64_t *times = db->tasks[idx].times; + totals[0] = add_int64(totals[0], times[0]); + totals[1] = add_int64(totals[1], times[1]); + totals[2] = add_int64(totals[2], times[2]); + totals[3] = add_int64(totals[3], times[3]); + totals[4] = add_int64(totals[4], times[4]); + totals[5] = add_int64(totals[5], times[5]); + totals[6] = add_int64(totals[6], times[6]); + } +} + +// Resets the times of the provided task (and adjusts database totals). +void reset_task_times(database_st *db, task_st *task) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task - db->tasks < db->count); + + // Make sure we sync before applying the changes. + update_times(db); + + for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + int64_t *timer = &task->times[idx]; + int64_t *total = &db->total_times[idx]; + *total = sub_int64(*total, *timer); + *timer = 0; + } +} + +// Sets the time on the day and task provided (and adjusts database totals). +void set_task_time(database_st *db, task_st *task, int day, int64_t time) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task - db->tasks < db->count); + + // Make sure we sync before applying the changes. + update_times(db); + + int64_t *timer = &task->times[day]; + int64_t *total = &db->total_times[day]; + *total = sub_int64(*total, *timer); + *timer = time; + *total = add_int64(*total, *timer); +} + +// Adds the time on the day and task provided (and adjusts database totals). +void add_task_time(database_st *db, task_st *task, int day, int64_t time) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task - db->tasks < db->count); + + // Make sure we sync before applying the changes. + update_times(db); + + task->times[day] = add_int64(task->times[day], time); + db->total_times[day] = add_int64(db->total_times[day], time); +} + +// Resets database to the initial state and deallocates all memory taken by tasks. +void reset_database(database_st *db) { + assert(db != NULL); + + free(db->tasks); + memset(db, 0, SIZEOF_DATABASE_ST); + db->active_task = -1; + db->selected_task = -1; +} + +// Stores data from database into binary file. +// Returns success. +bool store_database(const database_st *db, const char *path) { + assert(db != NULL); + assert(path != NULL); + + // Open file. + FILE *file = fopen(path, "wb"); + if (file == NULL) { + print_error("Failed to open file '%s' while storing database: %s.", path, strerror(errno)); + return false; + } + + fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); + fwrite(db, SIZEOF_DATABASE_ST, 1, file); + fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file); + + fclose(file); + return true; +} + +// Loads data from binary file into database. +// Returns success. +bool load_database(database_st *db, const char *path) { + assert(db != NULL); + assert(path != NULL); + + // Open file. + FILE *file = fopen(path, "rb"); + if (file == NULL) { + print_error("Failed to open file '%s' while loading database: %s.", path, strerror(errno)); + return false; + } + + // Validate file signature. + char file_signature[DB_FILE_SIGN_LENGTH]; + fread(&file_signature, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); + if (strncmp(file_signature, DB_FILE_SIGN, DB_FILE_SIGN_LENGTH) != 0) { + print_error("Invalid file signature."); + fclose(file); + return false; + } + + // Read database structure. + fread(db, SIZEOF_DATABASE_ST, 1, file); + + // Reserve database capacity for tasks. + size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; + db->tasks = malloc(capacity_bytes); + if (db->tasks == NULL && capacity_bytes > 0) { + print_error("Failed to allocate memory while loading database: %s.", strerror(errno)); + return false; + } + + // Read database tasks. + fread(db->tasks, SIZEOF_TASK_ST, db->count, file); + + // Make sure we are reading all the file. + assert(fgetc(file) == EOF); + + fclose(file); + return true; +} + +// Exports data into CSV file. +// Returns success. +bool export_to_csv(const database_st *db, const char *path) { + assert(db != NULL); + assert(path != NULL); + + FILE *file = fopen(path, "w"); + if (file == NULL) { + print_error("Failed to open file '%s' while exporting to CSV: %s.", path, strerror(errno)); + return false; + } + + fprintf(file, "%s,%s,%s,%s,%s,%s,%s,%s\n", + "task", "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" + ); + + char name[TASK_NAME_BYTES]; + for (size_t idx = 0; idx < db->count; idx++) { + task_st *task = &db->tasks[idx]; + memcpy(name, task->name, TASK_NAME_BYTES); + replace_char(name, ',', ' '); + fprintf(file, "%s,%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 "\n", + name, task->times[0], task->times[1], task->times[2], task->times[3], task->times[4], task->times[5], task->times[6] + ); + } + + fclose(file); + return true; +} + +// Imports CSV file into database. +// Returns success. +bool import_from_csv(database_st *db, const char *path) { + assert(db != NULL); + assert(path != NULL); + + FILE *file = fopen(path, "r"); + if (file == NULL) { + print_error("Failed to open file '%s' while importing from CSV: %s.", path, strerror(errno)); + return false; + } + + // Skip header line. + fscanf(file, "%*[^\n]\n"); + + // Parse CSV file. + char *csv_buffer = NULL; + size_t csv_buffer_size = 0; + while(getline(&csv_buffer, &csv_buffer_size, file) != -1) { // Check if reached EOF. + + // Find task name string limits. + char *name_delimiter = strchr(csv_buffer, ','); + if (name_delimiter == NULL) { + continue; + } + size_t name_length = (name_delimiter - csv_buffer); + if (name_length > TASK_NAME_LENGTH) { + name_length = TASK_NAME_LENGTH; + } + + // Prepare new task. + task_st *task; + if (create_task(db, &task) == false) { + return false; + } + + // Import task name. + memcpy(task->name, csv_buffer, name_length); + truncate_string_utf8(task->name, name_length); + + // Parse task times. + if (sscanf(name_delimiter + 1, + "%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64, + &task->times[0], &task->times[1], &task->times[2], &task->times[3], &task->times[4], &task->times[5], &task->times[6] + ) != NUM_WEEK_DAYS + ) { + replace_char(csv_buffer, '\n', ' '); + print_error("Discarding invalid line '%s' and continuing.", csv_buffer); + delete_task(db, task); + continue; + } + + // Add task timer values to total timers. + for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + db->total_times[idx] = add_int64(db->total_times[idx], task->times[idx]); + } + } + + fclose(file); + free(csv_buffer); + return true; +} + +// Appends task to the end of the CSV file. +// Returns success. +bool append_to_csv(task_st *task, const char *path) { + assert(task != NULL); + assert(path != NULL); + + FILE *file = fopen(path, "a+"); + if (file == NULL) { + print_error("Failed to open file '%s' while appending to CSV: %s.", path, strerror(errno)); + return false; + } + + char last_char; + fseek(file, -1, SEEK_END); + fread(&last_char, SIZEOF_CHAR, 1, file); + if (last_char != '\n') { + fprintf(file, "\n"); + } + + char name[TASK_NAME_BYTES]; + memcpy(name, task->name, TASK_NAME_BYTES); + replace_char(name, ',', ' '); + fprintf(file, "%s,%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 "\n", + name, task->times[0], task->times[1], task->times[2], task->times[3], task->times[4], task->times[5], task->times[6] + ); + + fclose(file); + return true; +} + +// Selects task by index. +// Index gets clamped to [0, db->count[. +void select_task_by_index(database_st *db, ptrdiff_t index) { + assert(db != NULL); + db->selected_task = db->count == 0 ? -1 + : index < 0 ? 0 + : index >= db->count ? db->count - 1 + : index; +} + +// Selects task by delta relative to currently selected task. +void select_task_by_delta(database_st *db, ptrdiff_t delta) { + assert(db != NULL); + ptrdiff_t idx = (delta > 0 && db->selected_task > PTRDIFF_MAX - delta) ? PTRDIFF_MAX + : (delta < 0 && db->selected_task < PTRDIFF_MIN + delta) ? PTRDIFF_MIN + : db->selected_task + delta; + select_task_by_index(db, idx); +} + +// Selects task. +void select_task(database_st *db, task_st *task) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task - db->tasks < db->count); + db->selected_task = task - db->tasks; +} + +// Set active task. +// Passing task as NULL de-activates any previously active task. +void set_active_task(database_st *db, task_st *task) { + assert(db != NULL); + assert(task == NULL || (task >= db->tasks && task < &db->tasks[db->count])); + update_times(db); + db->active_task = (task == NULL) ? -1 : task - db->tasks; +} + +// Returns true when database is full. +bool is_database_full(database_st *db) { + assert(db != NULL); + return db->count >= MAX_DATABASE_TASKS; +} + +#define INPUT_TIMEOUT_MS 1000 +#define INPUT_AWAIT_INF -1 + +#define NUM_HEADER_ROWS 1 +#define NUM_FOOTER_ROWS 1 +#define NUM_COLUMNS 9 + +#define L_TITLE_IDX 0 +#define L_DAYS_IDX 1 +#define L_TOTAL_IDX 8 + +typedef enum { + L_NORMAL, + L_COMPACT, + NUM_LAYOUTS, +} layouts_et; + +typedef struct { + char *header; + int width; + int alignment_offset; + char alignment; +} column_st; + +typedef struct { + column_st columns[NUM_COLUMNS]; + char *archive_title; +} layout_st; + +layout_st layouts[NUM_LAYOUTS]; +int layout_tasks_rows; +bool is_terminal_too_small = true; + + +void initialize_tui() { + + // Normal layout. + layouts[L_NORMAL] = (layout_st) { + .archive_title = " Archive ", + .columns = { + { .header = " Task Time Tracker v" VERSION " ", .width = -1, .alignment = 'L' }, + { .header = " Sun ", .width = 7, .alignment = 'C' }, + { .header = " Mon ", .width = 7, .alignment = 'C' }, + { .header = " Tue ", .width = 7, .alignment = 'C' }, + { .header = " Wed ", .width = 7, .alignment = 'C' }, + { .header = " Thu ", .width = 7, .alignment = 'C' }, + { .header = " Fri ", .width = 7, .alignment = 'C' }, + { .header = " Sat ", .width = 7, .alignment = 'C' }, + { .header = " Total ", .width = 9, .alignment = 'C' }, + } + }; + + // Compact layout. + layouts[L_COMPACT] = (layout_st) { + .archive_title = " Archive ", + .columns = { + { .header = " TTT " VERSION " ", .width = -1, .alignment = 'L' }, + { .header = " S ", .width = 5, .alignment = 'C' }, + { .header = " M ", .width = 5, .alignment = 'C' }, + { .header = " T ", .width = 5, .alignment = 'C' }, + { .header = " W ", .width = 5, .alignment = 'C' }, + { .header = " T ", .width = 5, .alignment = 'C' }, + { .header = " F ", .width = 5, .alignment = 'C' }, + { .header = " S ", .width = 5, .alignment = 'C' }, + { .header = " # ", .width = 5, .alignment = 'C' }, + } + }; + + // Calculate alignment_offsets. + for (layout_st *layout = layouts; layout < layouts + NUM_LAYOUTS; layout++) { + for (column_st *col = layout->columns; col < layout->columns + NUM_COLUMNS; col++) { + int offset; + switch(col->alignment) { + default: + case 'L': + offset = 0; + break; + + case 'C': + offset = ((col->width - strlen(col->header)) / 2); + break; + + case 'R': + offset = (col->width - strlen(col->header)); + break; + } + col->alignment_offset = offset; + } + } + + setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. + initscr(); // Start curses mode. + cbreak(); // Line buffering disabled; pass on everty thing to me. + keypad(stdscr, TRUE); // I need that nifty F1. + curs_set(0); // Set cursor invisible. + noecho(); // Disable echoing input characters. + + // Initialize pairs of colors. + start_color(); + use_default_colors(); // Using default (-1) instead of COLOR_BLACK. + init_pair(STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN); + init_pair(STYLE_SELECTED_INVERTED, COLOR_CYAN, -1); + init_pair(STYLE_ACTIVE, COLOR_BLUE, -1); + init_pair(STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); + init_pair(STYLE_ERROR, COLOR_RED, -1); +} + +void 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_st *layout = layouts; layout <= &layouts[NUM_LAYOUTS - 1]; layout++) { + layout->columns[0].width = size_x - (NUM_COLUMNS - 1) - 2; + for (int idx = 1; idx < NUM_COLUMNS; idx++) { + layout->columns[0].width -= layout->columns[idx].width; + } + } +} + +void draw_tui(database_st *db, layout_st *layout) { + + const static int adjust_first_day_of_week[] = { + (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + (2 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + (3 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + (4 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + (5 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + (6 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, + }; + + int x, y; + column_st *col; + + // Get context information. + task_st *active_task = get_active_task(db); + task_st *selected_task = get_selected_task(db); + time_t now_utc = time(NULL); + int now_week_day = localtime(&now_utc)->tm_wday; + + // Reset theme and clear screen. + attrset(A_NORMAL); + erase(); + + // Draw outer border. + box(stdscr, 0, 0); + + // Draw table grids. + y = 0; + x = 0; + for (int idx = 0; idx < NUM_COLUMNS - 1; idx++) { + x += 1 + layout->columns[idx].width; + mvaddch(y, x, ACS_TTEE); + for (y = 1; y < size_y - 1; y++) { + mvaddch(y, x, ACS_VLINE); + } + mvaddch(size_y - 1, x, ACS_BTEE); + } + + + /////////////////////////////////////////////////////////////////////////// + // Draw headers. + y = 0; + x = 0; + + // Headers : title + x++; + col = &layout->columns[L_TITLE_IDX]; + mvaddstr(y, x + col->alignment_offset, (db == &archive ? layout->archive_title : col->header)); + x += col->width; + + // Headers : days + for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) { + int idx = adjust_first_day_of_week[raw_idx]; + x++; + + // Apply theme. + if (idx == now_week_day && active_task != NULL) { + attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); + } + else if (idx == now_week_day) { + attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD); + } + + col = &layout->columns[L_DAYS_IDX + idx]; + mvaddstr(y, x + col->alignment_offset, col->header); + x += col->width; + + // Reset theme. + attrset(A_NORMAL); + } + + // Headers : total + x++; + col = &layout->columns[L_TOTAL_IDX]; + mvaddstr(y, x + col->alignment_offset, col->header); + + + /////////////////////////////////////////////////////////////////////////// + // Draw tasks. + + uint64_t total_time = 0; + int column_width; + + y = 0; + // Pagination based on currently selected task (show page where selected task is). + size_t idx_start = (db->selected_task / layout_tasks_rows) * layout_tasks_rows; + // Display up to rows allowed by the layout, or less if reached end of database. + size_t idx_stop = idx_start + (layout_tasks_rows > db->count - idx_start ? db->count - idx_start : layout_tasks_rows); + for (size_t idx = idx_start; idx < idx_stop; idx++) { + task_st *task = &db->tasks[idx]; + y++; + x = 0; + + // Apply theme. + if (task == active_task && task == selected_task) { + attron(COLOR_PAIR(STYLE_ACTIVE_SELECTED) | A_BOLD); + } + else if (task == selected_task) { + attron(COLOR_PAIR(STYLE_SELECTED)); + } + else if (task == active_task) { + attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); + } + + // Task title. + x++; + column_width = layout->columns[L_TITLE_IDX].width; + mvprintw(y, x, "%-*.*s", column_width, column_width, task->name); + x += column_width; + + // Task times. + total_time = 0; + for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + x++; + + int day_idx = (idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + + column_width = layout->columns[L_DAYS_IDX + day_idx].width; + int64_t task_stime = task->times[day_idx]; + total_time = add_int64(total_time, task_stime); + mvprintw_time(y, x, task_stime, column_width); + x += column_width; + } + + // Task total. + x++; + mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width); + + // Reset theme. + attrset(A_NORMAL); + } + + + /////////////////////////////////////////////////////////////////////////// + // Draw selected/total tasks. + int size = snprintf(NULL, 0, " %td/%zd ", db->selected_task + 1, db->count); + if (size <= layout->columns[L_TITLE_IDX].width) { + mvprintw(size_y - 1, 1, " %td/%zd ", db->selected_task + 1, db->count); + } + else { + mvprintw(size_y - 1, 1, "%td", db->selected_task + 1); + } + + /////////////////////////////////////////////////////////////////////////// + // Draw daily totals. + y = size_y - 1; + x = 0 + 1 + layout->columns[L_TITLE_IDX].width; + total_time = 0; + for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) { + int idx = adjust_first_day_of_week[raw_idx]; + int64_t daily_total = db->total_times[idx]; + x++; + + // Apply theme. + if (idx == now_week_day && active_task != NULL) { + attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); + } + else if (idx == now_week_day) { + attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD); + } + + column_width = layout->columns[L_DAYS_IDX + idx].width; + total_time = add_int64(total_time, daily_total); + mvprintw_time(y, x, daily_total, column_width); + x += column_width; + + // Reset theme. + attrset(A_NORMAL); + } + x++; + mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width); +} + +void *mem_alloc(size_t mem_size, const char *error_tag) { + void *mem_pointer = malloc(mem_size); + if (mem_pointer == NULL && mem_size > 0) { + print_error("Failed to allocate memory (%s): %s.", (error_tag == NULL ? "undefined" : error_tag), strerror(errno)); + exit(EXIT_FAILURE); + } + return mem_pointer; +} + +void free_memory() { + reset_database(&database); + reset_database(&archive); + + free(string_buffer); string_buffer = NULL; + free(app_folder); app_folder = NULL; + free(db_file_path); db_file_path = NULL; + free(ar_file_path); ar_file_path = NULL; +} + +bool initialize_app_folder() { + size_t temp_size; + + char *home_path = getenv(HOME_PATH_ENV); + if (home_path == NULL) { + home_path = "."; + } + temp_size = strlen(home_path) + 1 + strlen(APP_FOLDER_NAME) + 1; // Add space for folder separator and '\0'. + app_folder = mem_alloc(temp_size, "app folder"); + snprintf(app_folder, temp_size, "%s/%s", home_path, APP_FOLDER_NAME); + + // Create app folder. + mkdir(app_folder, 0740); + if (errno != 0 && errno != EEXIST) { + print_error("Failed to create app folder '%s': %s.", app_folder, strerror(errno)); + return false; + } + + // Set database file path. + temp_size = strlen(app_folder) + 1 + strlen(DB_FILE_NAME) + 1; // Add space for folder separator and '\0'. + db_file_path = mem_alloc(temp_size, "database file path"); + snprintf(db_file_path, temp_size, "%s/%s", app_folder, DB_FILE_NAME); + + // Set archive file path. + temp_size = strlen(app_folder) + 1 + strlen(AR_FILE_NAME) + 1; // Add space for folder separator and '\0'. + ar_file_path = mem_alloc(temp_size, "archive file path"); + snprintf(ar_file_path, temp_size, "%s/%s", app_folder, AR_FILE_NAME); + + return true; +} + +void exit_gracefully(int signal) { + flushinp(); + ungetch('q'); +} + +void read_input_to_string_buffer_with_space(int row, int column, int style, int length, int space) { + assert(length < string_buffer_size); + assert(space < string_buffer_size); + + attron(style | A_UNDERLINE); + mvprintw(row, column, "%*s", space, ""); + echo(); + curs_set(1); + memset(string_buffer, 0, string_buffer_size); + mvgetnstr(row, column, string_buffer, length); + truncate_string_utf8(string_buffer, length); + noecho(); + curs_set(0); + attrset(A_NORMAL); +} + +void read_input_to_string_buffer(int row, int column, int style, int length) { + read_input_to_string_buffer_with_space(row, column, style, length, length); +} + +// Returns success. +bool read_input_to_int(int row, int style, const char *message, intmax_t *result) { + assert(message != NULL); + assert(result != NULL); + + attron(style); + move(row, 1); + addch(ACS_CKBOARD); + addstr(message); + attrset(A_NORMAL); + + // Get line number. + int input_pos_x = getcurx(stdscr); + int input_width = size_x - input_pos_x - 1; + read_input_to_string_buffer(row, input_pos_x, style, input_width); + + char *parser; + errno = 0; + *result = strtoimax(string_buffer, &parser, 10); + + bool success = (errno == 0 || errno == ERANGE) // No error OR value was clamped to limits (acceptable). + && parser != string_buffer; // If no digits are found, parser will return the address of the input string. + + return success; +} + +// Retuns true if user presses enter, false otherwise. +bool read_enter_confirmation(int row, int style, const char *message) { + assert(message != NULL); + + attron(style); + move(row, 1); + for (int idx = 0; idx < size_x - 2; idx++) { + addch(ACS_CKBOARD); + } + mvaddstr(row, 2, message); + attrset(A_NORMAL); + + return getch() == '\n'; +} + +int main(int argc, char *argv[]) { + + if (initialize_app_folder() == false) { + print_error("Failed to initialize app folder."); + free_memory(); + return EXIT_FAILURE; + } + + db = &database; + reset_database(&database); + reset_database(&archive); + + if (is_file_accessible(db_file_path) == false) { + if (store_database(&database, db_file_path) == false) { + print_error("Failed to initialize database."); + free_memory(); + return EXIT_FAILURE; + } + } + + if (is_file_accessible(ar_file_path) == false) { + if (export_to_csv(&archive, ar_file_path) == false) { + print_error("Failed to initialize archive."); + free_memory(); + return EXIT_FAILURE; + } + } + + if (argc > 1) { + bool is_exit_requested = false; + for (unsigned idx = 1; idx < argc; idx++) { + if (is_equal_to_any(argv[idx], "--help", "-h")) { + fprintf(stdout, + "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" + " -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" + " 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" + " n, N Create new task.\n" + " m, M Move selected task to position.\n" + " g, G Select task by position.\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" + "- All data files are stored in '$" HOME_PATH_ENV "/.task_time_tracker'.\n" + " If $" HOME_PATH_ENV " is undefined, './.task_time_tracker' will be used.\n" + " The database entries 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 tasks such as saving to file or recalculating totals times,\n" + " a diamond symbol is shown on the top left corner.\n" + ); + free_memory(); + return EXIT_SUCCESS; + } + + if (is_equal_to_any(argv[idx], "--version", "-v")) { + fprintf(stdout, + "Task Time Tracker version " VERSION "\n" + "Copyright 2022 Daniel Martins\n" + "License GPL-3.0-or-later\n" + ); + free_memory(); + return EXIT_SUCCESS; + } + + if (is_equal_to_any(argv[idx], "--import-csv", "-i")) { + idx++; + if (idx >= argc) { + print_error("Missing CSV file path to import."); + free_memory(); + return EXIT_FAILURE; + } + if (load_database(&database, db_file_path) == false) { + print_error("Failed to load database."); + free_memory(); + return EXIT_FAILURE; + } + if (import_from_csv(&database, argv[idx]) == false) { + print_error("Failed to import CSV file."); + free_memory(); + return EXIT_FAILURE; + } + if (store_database(&database, db_file_path) == false) { + print_error("Failed to store database."); + free_memory(); + return EXIT_FAILURE; + } + reset_database(&database); + is_exit_requested = true; + continue; + } + + if (is_equal_to_any(argv[idx], "--export-csv", "-e")) { + idx++; + if (idx >= argc) { + print_error("Missing CSV file path to export."); + free_memory(); + return EXIT_FAILURE; + } + if (load_database(&database, db_file_path) == false) { + print_error("Failed to load database."); + free_memory(); + return EXIT_FAILURE; + } + if (export_to_csv(&database, argv[idx]) == false) { + print_error("Failed to export CSV file."); + free_memory(); + return EXIT_FAILURE; + } + reset_database(&database); + is_exit_requested = true; + continue; + } + + if (is_equal_to_any(argv[idx], "--no-autosave", "-n")) { + is_autosave_enabled = false; + continue; + } + + print_error("%s: invalid option '%s'.\nTry '%s --help' for more information.", argv[0], argv[idx], argv[0]); + free_memory(); + return EXIT_FAILURE; + } + + if (is_exit_requested) { + free_memory(); + return EXIT_SUCCESS; + } + } + + if (load_database(&database, db_file_path) == false) { + print_error("Failed to load database."); + free_memory(); + return EXIT_FAILURE; + } + + initialize_tui(); + + signal(SIGTERM, exit_gracefully); + signal(SIGINT, exit_gracefully); + signal(SIGQUIT, exit_gracefully); + signal(SIGHUP, exit_gracefully); + + flushinp(); + ungetch(KEY_RESIZE); + for (int key; ((key = getch()) != 'q') && (key != 'Q'); ) { + + static layout_st *layout = &layouts[L_COMPACT]; + task_st *active_task = get_active_task(db); + task_st *selected_task = get_selected_task(db); + int action_style = A_BOLD | COLOR_PAIR(selected_task == active_task && selected_task != NULL ? STYLE_ACTIVE : STYLE_SELECTED_INVERTED); + int error_style = A_BOLD | COLOR_PAIR(STYLE_ERROR); + int selected_task_row = is_terminal_too_small ? 0 + : (db->selected_task < 0) ? 1 + : (db->selected_task % layout_tasks_rows) + NUM_HEADER_ROWS; + + timeout(INPUT_AWAIT_INF); + update_times(&database); + + switch(key) { + + // When getch() times out. + case ERR: { + if (is_autosave_enabled && countdown_to_autosave > 0) { + countdown_to_autosave -= INPUT_TIMEOUT_MS; + if (countdown_to_autosave <= 0) { + show_processing(); + if (db == &archive) { + export_to_csv(&archive, ar_file_path); + } + store_database(&database, db_file_path); + } + } + break; + } + + // When terminal is resized. + case KEY_RESIZE: { + clear(); + getmaxyx(stdscr, size_y, size_x); + is_terminal_too_small = size_x < 60 || size_y < 3; + size_t new_size = 2047 | TASK_NAME_BYTES | (size_x + 1); + if (string_buffer_size < new_size) { + string_buffer_size = new_size; + string_buffer = realloc(string_buffer, string_buffer_size); + if (string_buffer == NULL && string_buffer_size > 0) { + print_error("Failed to allocate memory for string buffer: %s.", strerror(errno)); + flushinp(); + ungetch('q'); + break; + } + } + update_layout(); + layout = &layouts[size_x > 100 ? L_NORMAL : L_COMPACT]; + break; + } + + case 'n': + case 'N':{ + if (is_database_full(db)) { + read_enter_confirmation(selected_task_row, error_style, " Unable to create entry: database is full. "); + break; + } + + // Create new task. + task_st *new_task; + if (create_task(db, &new_task) == false) { + break; + } + + // Set new task name. + time_t now_utc = time(NULL); + struct tm *now_local = localtime(&now_utc); + strftime(new_task->name, TASK_NAME_BYTES, "%Y-%m-%d %H:%M:%S", now_local); + + // Select new task. + select_task(db, new_task); + selected_task = get_selected_task(db); + trigger_autosave(); + + // Force rename action. + flushinp(); + ungetch(KEY_F(2)); + break; + } + + case KEY_F(2): { + if (selected_task == NULL) { + break; + } + + read_input_to_string_buffer_with_space(selected_task_row, 1, action_style, TASK_NAME_LENGTH, size_x - 2); + if (is_empty_string(string_buffer) == false) { + replace_char(string_buffer, '\t', ' '); + replace_char(string_buffer, '\v', ' '); + replace_char(string_buffer, '\f', ' '); + replace_char(string_buffer, '\r', ' '); + memcpy(selected_task->name, string_buffer, TASK_NAME_BYTES); + trigger_autosave(); + } + break; + } + + case KEY_BACKSPACE: { + if (selected_task == NULL) { + break; + } + + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) { + reset_task_times(db, selected_task); + trigger_autosave(); + } + break; + } + + case KEY_DC: { // Delete + if (selected_task == NULL || selected_task == active_task) { + break; + } + + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) { + delete_task(db, selected_task); + trigger_autosave(); + } + break; + } + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + if (selected_task == NULL) { + break; + } + + // Prepare position to input time operation. + int selected_day = key - '1'; + int input_width = layout->columns[L_DAYS_IDX + selected_day].width; + int input_pos_x = 1 + layout->columns[L_TITLE_IDX].width; + for (int col = 0; col < selected_day; col++) { + input_pos_x += 1 + layout->columns[L_DAYS_IDX + col].width; + } + input_pos_x++; + + // Get input string. + read_input_to_string_buffer(selected_task_row, input_pos_x, action_style, input_width); + char *input = string_buffer; + + // Abort if input if empty. + if (is_empty_string(input) == true) { + break; + } + + // Search for assign '=' operator and discard everything before (if found). + char *assign_str = strchr(input, '='); + bool is_assign = assign_str != NULL; + if (is_assign == true) { + input = assign_str + 1; + } + + // Try to parse a number and abort if it fails. + char *parser; + long double input_float = strtold(input, &parser); + if (parser == input) { + break; + } + input = parser; + + // Try to parse a character representing the time multiplier. + long double multiplier = 1.0; + for (int i=0; i < strlen(input); i++) { + char ch = input[i]; + if (ch == 'm' || ch == 'M') { + multiplier = SECONDS_IN_MINUTE; + break; + } + else if (ch == 'h' || ch == 'H') { + multiplier = SECONDS_IN_HOUR; + break; + } + else if (ch == 'd' || ch == 'D') { + multiplier = SECONDS_IN_DAY; + break; + } + else if (ch == 'y' || ch == 'Y') { + multiplier = SECONDS_IN_YEAR; + break; + } + } + + // Process input and check if it's valid. + long double input_time = input_float * multiplier; + bool is_result_valid = (input_time >= (long double)INT64_MIN && input_time <= (long double)INT64_MAX); + if (is_result_valid == false) { + break; + } + + // Apply changes. + int64_t time = input_time; + int day = (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + if (is_assign == true) { + set_task_time(db, selected_task, day, time); + } + else { + add_task_time(db, selected_task, day, time); + } + trigger_autosave(); + break; + } + + case 'm': + case 'M': { + if (selected_task == NULL) { + break; + } + + intmax_t value; + if (read_input_to_int(selected_task_row, action_style, " Move to: ", &value) == false) { + break; + } + + ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1; + move_task_to_index(db, selected_task, target_index); + trigger_autosave(); + break; + } + + case 'g': + case 'G': { + if (selected_task == NULL) { + break; + } + + intmax_t value; + if (read_input_to_int(selected_task_row, action_style, " Go to: ", &value) == false) { + break; + } + + ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1; + select_task_by_index(db, target_index); + break; + } + + case 'd': + case 'D':{ + if (selected_task == NULL) { + break; + } + + if (is_database_full(db)) { + read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate entry: database is full. "); + break; + } + + if (duplicate_task(db, selected_task) == false) { + break; + } + trigger_autosave(); + break; + } + + case KEY_F(5): { + update_total_times(db); + trigger_autosave(); + break; + } + + case 't': + case 'T': { + if (active_task == NULL) { + break; + } + select_task(db, active_task); + break; + } + + case '\n': + case ' ': { + if (db != &database || selected_task == NULL) { + break; + } + set_active_task(db, (active_task == selected_task) ? NULL : selected_task); + active_task = get_active_task(db); + trigger_autosave(); + break; + } + + case '\t': { + if (db == &database) { + if (import_from_csv(&archive, ar_file_path) == false) { + reset_database(&archive); + print_error("Failed to load archive."); + break; + } + db = &archive; + } + else { + if (export_to_csv(&archive, ar_file_path) == false) { + print_error("Failed to store archive."); + break; + } + reset_database(&archive); + db = &database; + } + break; + } + + case 'a': + case 'A': { + if (db != &database || selected_task == NULL || selected_task == active_task) { + break; + } + if (append_to_csv(selected_task, ar_file_path) == false) { + print_error("Failed to archive entry."); + break; + } + delete_task(&database, selected_task); + trigger_autosave(); + break; + } + + case 'r': + case 'R': { + if (db != &archive || selected_task == NULL) { + break; + } + if (is_database_full(&database)) { + read_enter_confirmation(selected_task_row, error_style, " Unable to restore entry: database is full. "); + break; + } + if (duplicate_task(&database, selected_task) == false) { + print_error("Failed to restore entry."); + break; + } + delete_task(&archive, selected_task); + trigger_autosave(); + break; + } + + case KEY_HOME: { + select_task_by_index(db, 0); + break; + } + + case KEY_UP: { + select_task_by_delta(db, -1); + break; + } + + case KEY_PPAGE: { + select_task_by_delta(db, -layout_tasks_rows); + break; + } + + case KEY_END: { + select_task_by_index(db, db->count-1); + break; + } + + case KEY_DOWN: { + select_task_by_delta(db, 1); + break; + } + + case KEY_NPAGE: { + select_task_by_delta(db, layout_tasks_rows); + break; + } + } + + if (is_terminal_too_small) { + const char *INVALID_WINDOW_MESSAGE = "Terminal is too small: minimum 60x3."; + const int INVALID_WINDOW_MESSAGE_LENGTH = strlen(INVALID_WINDOW_MESSAGE); + mvaddstr(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE_LENGTH) / 2, INVALID_WINDOW_MESSAGE); + } + else { + draw_tui(db, layout); + draw_error_window(); + } + + timeout(INPUT_TIMEOUT_MS); + } + + // Save any unsaved changes. + show_processing(); + bool error_saving = false; + if (db == &archive) { + if (export_to_csv(&archive, ar_file_path) == false) { + print_error("Failed to save archive."); + error_saving |= true; + } + } + if (countdown_to_autosave > 0 || is_autosave_enabled == false) { + if (store_database(&database, db_file_path) == false) { + print_error("Failed to save database."); + error_saving |= true; + } + } + if (error_saving) { + print_error("Press any key to close."); + draw_error_window(); + timeout(INPUT_AWAIT_INF); + getch(); + } + + endwin(); + free_memory(); + return error_saving ? EXIT_FAILURE : EXIT_SUCCESS; +} +*/ + +#import "Basic"; + +main :: () { + print("Task Time Tracker version %\n", VERSION); + print("Copyright 2022 Daniel Martins\n"); + print("License GPL-3.0-or-later\n"); +} -- cgit v1.2.3 From bb62e6e9f46f2009a9b09d8b3f106a7470ffb0f2 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 25 Jan 2023 00:01:15 +0000 Subject: Initial tests in jai. --- ttt.jai | 116 +++++++++++++++++++++++----------------------------------------- 1 file changed, 42 insertions(+), 74 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index a2a4e8f..fd39bb3 100644 --- a/ttt.jai +++ b/ttt.jai @@ -14,90 +14,47 @@ // this program. If not, see . -// Compilation commands: -// - debug : jai ttt.jai -exe ttt -// - release : jai ttt.jai -exe ttt -x64 -release -// -// Compiler flags: -// -l : libraries to link -// -o : output file name -// -Wall : enables all compiler's warning messages -// -Werror : make all warnings into errors -// -pedantic : issue all the warnings demanded by strict ISO C -// -O : code optimization level (commonly accepted as best: 2) -// -g : debug information level (max: 3) -// -m64 : 64b architecture -// -D : defines for preprocessor -// -static-pie : link statically producing an position-independent executable -// -DNDEBUG : remove assertions from code (not used) - -/* -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -*/ - -// #define VERSION "2.0" // Use only 3 chars (to fit layouts). -VERSION :: "2.0"; -// #define TASK_NAME_LENGTH 57 // Task name length. +VERSION :: "2.0"; // Use only 3 chars (to fit layouts). TASK_NAME_LENGTH :: 57; -// #define TASK_NAME_BYTES (TASK_NAME_LENGTH+1) -TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; -// #define FIRST_DAY_OF_WEEK 1 // (0-6, Sunday = 0). -FIRST_DAY_OF_WEEK :: 1; -// #define NUM_WEEK_DAYS 7 // Just to be more clear about what we're looping about. -NUM_WEEK_DAYS :: 7; +TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; // TODO Get rid of this! +FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). +NUM_WEEK_DAYS :: 7; // Just to be more clear about what we're looping about. + +APP_FOLDER_NAME :: ".task_time_tracker"; +DB_FILE_NAME :: "database.bin"; +AR_FILE_NAME :: "archive.csv"; + +DB_FILE_SIGN_STR :: "TTT:B:02"; +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 : [TASK_NAME_BYTES] u8; // TODO Start using strings. +} -/* -#if defined(_WIN64) -#define HOME_PATH_ENV "USERPROFILE" -#else -#define HOME_PATH_ENV "HOME" -#endif -#define APP_FOLDER_NAME ".task_time_tracker" -#define DB_FILE_NAME "database.bin" -#define AR_FILE_NAME "archive.csv" +Database :: struct { + active_task : *~s64 Task = null; + selected_task : *~s64 Task = null; + tasks : [..] Task; + modified_on : s64; + total_times : [NUM_WEEK_DAYS] s64; +// size_t count; // Will always be equal or less than capacity. TODO Will this be necessary? +} -typedef struct { - int64_t times[NUM_WEEK_DAYS]; - char name[TASK_NAME_BYTES]; -} task_st; -typedef struct { - task_st *tasks; - size_t count; // Will always be equal or less than capacity. - size_t capacity; // Will always be equal or less than PTRDIFF_MAX (see MAX_DATABASE_TASKS). - ptrdiff_t active_task; // Will always be less than capacity/count. - ptrdiff_t selected_task; // Will always be less than capacity/count. - int64_t modified_on; - int64_t total_times[NUM_WEEK_DAYS]; -} database_st; - -#define DB_FILE_SIGN_STR "TTT:B:01" + +/* const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; const size_t SIZEOF_TASK_ST = sizeof(task_st); const size_t SIZEOF_DATABASE_ST = sizeof(database_st); const size_t SIZEOF_CHAR = sizeof(char); const size_t SIZEOF_INT64 = sizeof(int64_t); -const int64_t SECONDS_IN_MINUTE = (int64_t)60; -const int64_t SECONDS_IN_HOUR = (int64_t)60*SECONDS_IN_MINUTE; -const int64_t SECONDS_IN_DAY = (int64_t)24*SECONDS_IN_HOUR; -const int64_t SECONDS_IN_YEAR = (int64_t)365*SECONDS_IN_DAY; -const size_t MAX_DATABASE_TASKS = (PTRDIFF_MAX < (SIZE_MAX / SIZEOF_TASK_ST)) ? PTRDIFF_MAX : (SIZE_MAX / SIZEOF_TASK_ST); + database_st database = { .tasks = NULL }; @@ -1865,8 +1822,19 @@ int main(int argc, char *argv[]) { */ #import "Basic"; +#import "System"; +#import "File_Utilities"; + +homie : string; main :: () { + + print("TNL %\n", TASK_NAME_LENGTH); + print("TNB %\n", TASK_NAME_BYTES); + + home, success := get_home_directory(); + print("home '%' | success '%'\n", home, success); + print("Task Time Tracker version %\n", VERSION); print("Copyright 2022 Daniel Martins\n"); print("License GPL-3.0-or-later\n"); -- cgit v1.2.3 From e7cdf6fe804e1c01179afc3e40b761144cd0b057 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 25 Jan 2023 00:26:24 +0000 Subject: Ported add_int64 and sub_int64 to jai. --- ttt.jai | 39 ++++++++++++--------------------------- unused.jai | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 27 deletions(-) create mode 100644 unused.jai (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index fd39bb3..ea999c8 100644 --- a/ttt.jai +++ b/ttt.jai @@ -271,39 +271,23 @@ int mvprintw_time(int y, int x, intmax_t time, int space) { return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); } } +*/ -int64_t add_int64(int64_t x, int64_t y) { - int64_t result; -#ifdef __GNUC__ - bool overflow = __builtin_add_overflow(x, y, &result); - if (overflow) { - result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN) - } -#else - result = - (y > 0 && x > INT64_MAX - y) ? INT64_MAX : - (y < 0 && x < INT64_MIN - y) ? INT64_MIN : +add_int64 :: (x :s64, y: s64) -> s64 { + return + ifx (y > 0 && x > S64_MAX - y) then S64_MAX else + ifx (y < 0 && x < S64_MIN - y) then S64_MIN else x + y; -#endif - return result; } -int64_t sub_int64(int64_t x, int64_t y) { - int64_t result; -#ifdef __GNUC__ - bool overflow = __builtin_sub_overflow(x, y, &result); - if (overflow) { - result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN) - } -#else - result = - (y < 0 && x > INT64_MAX + y) ? INT64_MAX : - (y > 0 && x < INT64_MIN + y) ? INT64_MIN : +sub_int64 :: (x :s64, y :s64) -> s64 { + return + ifx (y < 0 && x > S64_MAX + y) then S64_MAX else + ifx (y > 0 && x < S64_MIN + y) then S64_MIN else x - y; -#endif - return result; } +/* // Returns active task or NULL if none applies. task_st *get_active_task(database_st *db) { assert(db != NULL); @@ -1823,7 +1807,8 @@ int main(int argc, char *argv[]) { #import "Basic"; #import "System"; -#import "File_Utilities"; +#import "Math"; +// #import "File_Utilities"; homie : string; diff --git a/unused.jai b/unused.jai new file mode 100644 index 0000000..3529a46 --- /dev/null +++ b/unused.jai @@ -0,0 +1,50 @@ +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; +} + -- cgit v1.2.3 From 43d2bf3d44ebdd50c535649f609b458d1b487abc Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 27 Jan 2023 01:56:40 +0000 Subject: First attempt to use ncurses. --- ttt.jai | 97 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index ea999c8..4911cc1 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1823,4 +1823,101 @@ main :: () { print("Task Time Tracker version %\n", VERSION); print("Copyright 2022 Daniel Martins\n"); print("License GPL-3.0-or-later\n"); + +// TODO More binding examples here https://github.com/vrcamillo/jai-tracy +/* + short : s16 + int : s32 + long : s64 (int) + single : float32 (float) + double : float64 +*/ + +ldat :: struct { + text :*void; /* text of the line */ + firstchar :s16; /* first changed character in the line */ + lastchar :s16; /* last changed character in the line */ + oldindex :s16; /* index of the line at last update */ +}; + + +WINDOW :: struct { + _cury, _curx : s16; /* current cursor position */ + + /* window location and size */ + _maxy, _maxx : s16; /* maximums of x and y, NOT window size */ + _begy, _begx : s16; /* screen coords of upper-left-hand corner */ + + _flags :s16; /* window state flags */ + + /* attribute tracking */ + _attrs :u8; /* current attribute for non-space character */ + _bkgd :u8; /* current background char/attribute pair */ + + /* option values set by user */ + _notimeout :bool; /* no time out on function-key entry? */ + _clear :bool; /* consider all data in the window invalid? */ + _leaveok :bool; /* OK to not reset cursor on exit? */ + _scroll :bool; /* OK to scroll this window? */ + _idlok :bool; /* OK to use insert/delete line? */ + _idcok :bool; /* OK to use insert/delete char? */ + _immed :bool; /* window in immed mode? (not yet used) */ + _sync :bool; /* window in sync mode? */ + _use_keypad :bool; /* process function keys into KEY_ symbols? */ + _delay :s32; /* 0 = nodelay, <0 = blocking, >0 = delay */ + + _line :*ldat; /* the actual line data */ + + /* global screen state */ + _regtop :s16; /* top line of scrolling region */ + _regbottom :s16; /* bottom line of scrolling region */ + + /* these are used only if this is a sub-window */ + _parx :s32; /* x coordinate of this window in parent */ + _pary :s32; /* y coordinate of this window in parent */ + _parent :*WINDOW; /* pointer to parent if a sub-window */ + + /* these are used only if this is a pad */ + pdat :: struct + { + _pad_y, _pad_x :s16 ; + _pad_top, _pad_left :s16; + _pad_bottom, _pad_right :s16; + }; + _pad :pdat; + + _yoffset :s16; /* real begy is _begy + _yoffset */ + +/* +#if NCURSES_WIDECHAR + cchar_t _bkgrnd; +#if 1 + int _color; +#endif +#endif +*/ +}; + + tinfo :: #system_library "libtinfo"; // Required by some of ncurses functions. + ncurses :: #library "libncurses"; + initscr :: () -> *WINDOW #foreign ncurses; + getch :: () -> s8 #foreign ncurses; + endwin :: () -> void #foreign ncurses; + + curs_set :: (visibility: s32) -> s32 #foreign ncurses; + mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; + noecho :: () -> s32 #foreign ncurses; + box :: (win :*WINDOW, verch :u8, horch :u8) -> s32 #foreign ncurses; + + stdscr := initscr(); + curs_set(0); + noecho(); + box(stdscr, 0, 0); + while true { + key := getch(); + if key == #char "q" break; + mvaddstr(1, 1, "> wow alçapão <"); + } + endwin(); + } -- cgit v1.2.3 From e952ef4d14589d8dbaf2f6e8cf0399030b725bff Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 27 Jan 2023 17:41:08 +0000 Subject: Initial attempt to process input arguments. Added fossil settings. --- .fossil-settings/binary-glob | 15 +++ .fossil-settings/crlf-glob | 1 + .fossil-settings/ignore-glob | 261 +++++++++++++++++++++++++++++++++++++++++++ curses.jai | 89 +++++++++++++++ libncurses.so | Bin 0 -> 165808 bytes ttt.jai | 259 +++++++++++++++++------------------------- 6 files changed, 467 insertions(+), 158 deletions(-) create mode 100644 .fossil-settings/binary-glob create mode 100644 .fossil-settings/crlf-glob create mode 100644 .fossil-settings/ignore-glob create mode 100644 curses.jai create mode 100644 libncurses.so (limited to 'ttt.jai') diff --git a/.fossil-settings/binary-glob b/.fossil-settings/binary-glob new file mode 100644 index 0000000..21f2881 --- /dev/null +++ b/.fossil-settings/binary-glob @@ -0,0 +1,15 @@ +*.ico +*.keystore +*.mp3 +*.mp4 +*.mkv +*.otf +*.png +*.so +*.tar.gz +*.tar.zst +*.tgz +*.ttf +*.wav +*.xcf +*.zip diff --git a/.fossil-settings/crlf-glob b/.fossil-settings/crlf-glob new file mode 100644 index 0000000..92bc64d --- /dev/null +++ b/.fossil-settings/crlf-glob @@ -0,0 +1 @@ +CR+LF diff --git a/.fossil-settings/ignore-glob b/.fossil-settings/ignore-glob new file mode 100644 index 0000000..3c4efe2 --- /dev/null +++ b/.fossil-settings/ignore-glob @@ -0,0 +1,261 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +#*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc \ No newline at end of file diff --git a/curses.jai b/curses.jai new file mode 100644 index 0000000..bec9db5 --- /dev/null +++ b/curses.jai @@ -0,0 +1,89 @@ + +ldat :: struct { + text :*void; /* text of the line */ + firstchar :s16; /* first changed character in the line */ + lastchar :s16; /* last changed character in the line */ + oldindex :s16; /* index of the line at last update */ +}; + +// TODO This also works... +WINDOW :: struct { + data : [88] u8; +} + +WINDOWx :: struct { + _cury, _curx : s16; /* current cursor position */ + + /* window location and size */ + _maxy, _maxx : s16; /* maximums of x and y, NOT window size */ + _begy, _begx : s16; /* screen coords of upper-left-hand corner */ + + _flags :s16; /* window state flags */ + + /* attribute tracking */ + _attrs :u8; /* current attribute for non-space character */ + _bkgd :u8; /* current background char/attribute pair */ + + /* option values set by user */ + _notimeout :bool; /* no time out on function-key entry? */ + _clear :bool; /* consider all data in the window invalid? */ + _leaveok :bool; /* OK to not reset cursor on exit? */ + _scroll :bool; /* OK to scroll this window? */ + _idlok :bool; /* OK to use insert/delete line? */ + _idcok :bool; /* OK to use insert/delete char? */ + _immed :bool; /* window in immed mode? (not yet used) */ + _sync :bool; /* window in sync mode? */ + _use_keypad :bool; /* process function keys into KEY_ symbols? */ + _delay :s32; /* 0 = nodelay, <0 = blocking, >0 = delay */ + + _line :*ldat; /* the actual line data */ + + /* global screen state */ + _regtop :s16; /* top line of scrolling region */ + _regbottom :s16; /* bottom line of scrolling region */ + + /* these are used only if this is a sub-window */ + _parx :s32; /* x coordinate of this window in parent */ + _pary :s32; /* y coordinate of this window in parent */ + _parent :*WINDOW; /* pointer to parent if a sub-window */ + + /* these are used only if this is a pad */ + pdat :: struct + { + _pad_y, _pad_x :s16 ; + _pad_top, _pad_left :s16; + _pad_bottom, _pad_right :s16; + }; + _pad :pdat; + + _yoffset :s16; /* real begy is _begy + _yoffset */ + +/* +#if NCURSES_WIDECHAR + cchar_t _bkgrnd; +#if 1 + int _color; +#endif +#endif +*/ +}; + + +tinfo :: #system_library "libtinfo"; // Required by some of ncurses functions. +// NOTE +// Must use local lib while having package libncurses-dev installed because it introduces +// an override file: +// > cat /lib/x86_64-linux-gnu/libncurses.so +// INPUT(libncursesw.so.6 -ltinfo) +// I suspect this is an attempt to help the compilers to include libtinfo automatically. +// ncurses :: #system_library "libncurses"; +ncurses :: #library "libncurses"; +initscr :: () -> *WINDOW #foreign ncurses; +getch :: () -> s8 #foreign ncurses; +endwin :: () -> void #foreign ncurses; + +curs_set :: (visibility: s32) -> s32 #foreign ncurses; +mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; +noecho :: () -> s32 #foreign ncurses; +box :: (win :*WINDOW, verch :u8, horch :u8) -> s32 #foreign ncurses; + diff --git a/libncurses.so b/libncurses.so new file mode 100644 index 0000000..1e7352e Binary files /dev/null and b/libncurses.so differ diff --git a/ttt.jai b/ttt.jai index 4911cc1..c77f2a3 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1,4 +1,4 @@ -// Copyright 2022 Daniel Martins +// Copyright 2023 Daniel Martins // License GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it under @@ -14,6 +14,12 @@ // this program. If not, see . +#import "Basic"; +#import "System"; +#import "Math"; +#import "curses"; +// #import "File_Utilities"; + VERSION :: "2.0"; // Use only 3 chars (to fit layouts). TASK_NAME_LENGTH :: 57; TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; // TODO Get rid of this! @@ -159,13 +165,14 @@ bool is_file_accessible(const char *path) { } return is_file_accessible; } +*/ // Returns true if string to_compare is equal to any of the other passed strings, false otherwise. -bool is_equal_to_any(const char *to_compare, const char *test_a, const char *test_b) { - return strncmp(to_compare, test_a, strlen(test_a)+1) == 0 - || strncmp(to_compare, test_b, strlen(test_b)+1) == 0; +is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool { + return to_compare == test_a || to_compare == test_b; } +/* // Given an UTF8 encoded string, truncate it to length bytes without breaking any UTF8 character. // The string should have capacity for at least length + 1. // The terminating null byte ('\0') is not included in length. @@ -1117,7 +1124,9 @@ void *mem_alloc(size_t mem_size, const char *error_tag) { return mem_pointer; } -void free_memory() { +*/ +free_memory :: () { + /* TODO reset_database(&database); reset_database(&archive); @@ -1125,8 +1134,10 @@ void free_memory() { free(app_folder); app_folder = NULL; free(db_file_path); db_file_path = NULL; free(ar_file_path); ar_file_path = NULL; + */ } +/* bool initialize_app_folder() { size_t temp_size; @@ -1223,9 +1234,11 @@ bool read_enter_confirmation(int row, int style, const char *message) { return getch() == '\n'; } +*/ -int main(int argc, char *argv[]) { - + +main :: () { + /* if (initialize_app_folder() == false) { print_error("Failed to initialize app folder."); free_memory(); @@ -1251,72 +1264,84 @@ int main(int argc, char *argv[]) { return EXIT_FAILURE; } } + */ - if (argc > 1) { - bool is_exit_requested = false; - for (unsigned idx = 1; idx < argc; idx++) { - if (is_equal_to_any(argv[idx], "--help", "-h")) { - fprintf(stdout, - "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" - " -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" - " 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" - " n, N Create new task.\n" - " m, M Move selected task to position.\n" - " g, G Select task by position.\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" - "- All data files are stored in '$" HOME_PATH_ENV "/.task_time_tracker'.\n" - " If $" HOME_PATH_ENV " is undefined, './.task_time_tracker' will be used.\n" - " The database entries 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 tasks such as saving to file or recalculating totals times,\n" - " a diamond symbol is shown on the top left corner.\n" - ); + args := get_command_line_arguments(); + defer array_reset(*args); + + + + for args { + if it_index == 0 continue; + + + } + + if args.count > 1 { + + is_exit_requested := false; + for 1..args.count-1 { + if is_equal_to_any(args[it], "--help", "-h") { + print("Usage: ttt [OPTION]... [FILE]...\n"); + print(" -i, --import-csv [FILE] Import CSV file to database (discard first row).\n"); + print(" -e, --export-csv [FILE] Export database to CSV file.\n"); + print(" -n, --no-autosave Disable autosave feature (only save on exit).\n"); + print(" -h, --help Display this help and exit.\n"); + print(" -v, --version Output version information and exit.\n"); + print("\n"); + print("In app commands\n"); + print(" a, A Archive selected task (except if active).\n"); + print(" r, R Restore selected task from archive.\n"); + print(" t, T Select currently active task (if any).\n"); + print(" d, D Duplicate selected task.\n"); + print(" n, N Create new task.\n"); + print(" m, M Move selected task to position.\n"); + print(" g, G Select task by position.\n"); + print(" q, Q Save changes and exit.\n"); + print(" F2 Rename selected task.\n"); + print(" F5 Recalculate total times.\n"); + print(" TAB Toggle archive view.\n"); + print(" BACKSPACE Reset times for selected task.\n"); + print(" DELETE Delete selected task (except if active).\n"); + print(" SPACE, ENTER Toggle selected task as active/inactive.\n"); + print(" 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n"); + print(" =# sets # seconds;\n"); + print(" -# subtracts # seconds;\n"); + print(" # adds # seconds;\n"); + print(" #m specifies # as minutes;\n"); + print(" #h specifies # as hours;\n"); + print(" #d specifies # as days;\n"); + print(" #y specifies # as years.\n"); + print(" UP Select task above.\n"); + print(" DOWN Select task below.\n"); + print(" PAGE-UP Select task 1 page above.\n"); + print(" PAGE-DOWN Select task 1 page below.\n"); + print(" HOME Select first/top task.\n"); + print(" END Select last/bottom task.\n"); + print("\n"); + print("Notes\n"); + print("- All data files are stored '%/.task_time_tracker'.\n", get_home_directory()); + print(" If the home directory is undefined, './.task_time_tracker' will be used.\n"); + print(" The database entries are stored in binary format on the 'database.bin' file.\n"); + print(" The archived entries are stored in CSV format on the 'archive.csv' file.\n"); + print("- During intensive tasks such as saving to file or recalculating totals times,\n"); + print(" a diamond symbol is shown on the top left corner.\n"); free_memory(); - return EXIT_SUCCESS; + return; } - if (is_equal_to_any(argv[idx], "--version", "-v")) { - fprintf(stdout, - "Task Time Tracker version " VERSION "\n" - "Copyright 2022 Daniel Martins\n" + }}// TODO To be removed once we continue our journey... + /* + if is_equal_to_any(args[it], "--version", "-v") { + print("Task Time Tracker version % \n" + "Copyright 2023 Daniel Martins\n" "License GPL-3.0-or-later\n" ); free_memory(); return EXIT_SUCCESS; } - if (is_equal_to_any(argv[idx], "--import-csv", "-i")) { + if (is_equal_to_any(args[it], "--import-csv", "-i")) { idx++; if (idx >= argc) { print_error("Missing CSV file path to import."); @@ -1328,7 +1353,7 @@ int main(int argc, char *argv[]) { free_memory(); return EXIT_FAILURE; } - if (import_from_csv(&database, argv[idx]) == false) { + if (import_from_csv(&database, args[it]) == false) { print_error("Failed to import CSV file."); free_memory(); return EXIT_FAILURE; @@ -1343,7 +1368,7 @@ int main(int argc, char *argv[]) { continue; } - if (is_equal_to_any(argv[idx], "--export-csv", "-e")) { + if (is_equal_to_any(args[it], "--export-csv", "-e")) { idx++; if (idx >= argc) { print_error("Missing CSV file path to export."); @@ -1355,7 +1380,7 @@ int main(int argc, char *argv[]) { free_memory(); return EXIT_FAILURE; } - if (export_to_csv(&database, argv[idx]) == false) { + if (export_to_csv(&database, args[it]) == false) { print_error("Failed to export CSV file."); free_memory(); return EXIT_FAILURE; @@ -1365,12 +1390,12 @@ int main(int argc, char *argv[]) { continue; } - if (is_equal_to_any(argv[idx], "--no-autosave", "-n")) { + if (is_equal_to_any(args[it], "--no-autosave", "-n")) { is_autosave_enabled = false; continue; } - print_error("%s: invalid option '%s'.\nTry '%s --help' for more information.", argv[0], argv[idx], argv[0]); + print_error("%s: invalid option '%s'.\nTry '%s --help' for more information.", argv[0], args[it], argv[0]); free_memory(); return EXIT_FAILURE; } @@ -1802,16 +1827,11 @@ int main(int argc, char *argv[]) { endwin(); free_memory(); return error_saving ? EXIT_FAILURE : EXIT_SUCCESS; + */ } -*/ - -#import "Basic"; -#import "System"; -#import "Math"; -// #import "File_Utilities"; -homie : string; +/* main :: () { print("TNL %\n", TASK_NAME_LENGTH); @@ -1825,90 +1845,12 @@ main :: () { print("License GPL-3.0-or-later\n"); // TODO More binding examples here https://github.com/vrcamillo/jai-tracy -/* - short : s16 - int : s32 - long : s64 (int) - single : float32 (float) - double : float64 -*/ - -ldat :: struct { - text :*void; /* text of the line */ - firstchar :s16; /* first changed character in the line */ - lastchar :s16; /* last changed character in the line */ - oldindex :s16; /* index of the line at last update */ -}; - - -WINDOW :: struct { - _cury, _curx : s16; /* current cursor position */ - - /* window location and size */ - _maxy, _maxx : s16; /* maximums of x and y, NOT window size */ - _begy, _begx : s16; /* screen coords of upper-left-hand corner */ - - _flags :s16; /* window state flags */ - - /* attribute tracking */ - _attrs :u8; /* current attribute for non-space character */ - _bkgd :u8; /* current background char/attribute pair */ - - /* option values set by user */ - _notimeout :bool; /* no time out on function-key entry? */ - _clear :bool; /* consider all data in the window invalid? */ - _leaveok :bool; /* OK to not reset cursor on exit? */ - _scroll :bool; /* OK to scroll this window? */ - _idlok :bool; /* OK to use insert/delete line? */ - _idcok :bool; /* OK to use insert/delete char? */ - _immed :bool; /* window in immed mode? (not yet used) */ - _sync :bool; /* window in sync mode? */ - _use_keypad :bool; /* process function keys into KEY_ symbols? */ - _delay :s32; /* 0 = nodelay, <0 = blocking, >0 = delay */ - - _line :*ldat; /* the actual line data */ - - /* global screen state */ - _regtop :s16; /* top line of scrolling region */ - _regbottom :s16; /* bottom line of scrolling region */ - - /* these are used only if this is a sub-window */ - _parx :s32; /* x coordinate of this window in parent */ - _pary :s32; /* y coordinate of this window in parent */ - _parent :*WINDOW; /* pointer to parent if a sub-window */ - - /* these are used only if this is a pad */ - pdat :: struct - { - _pad_y, _pad_x :s16 ; - _pad_top, _pad_left :s16; - _pad_bottom, _pad_right :s16; - }; - _pad :pdat; - - _yoffset :s16; /* real begy is _begy + _yoffset */ +// short : s16 +// int : s32 +// long : s64 (int) +// single : float32 (float) +// double : float64 -/* -#if NCURSES_WIDECHAR - cchar_t _bkgrnd; -#if 1 - int _color; -#endif -#endif -*/ -}; - - tinfo :: #system_library "libtinfo"; // Required by some of ncurses functions. - ncurses :: #library "libncurses"; - initscr :: () -> *WINDOW #foreign ncurses; - getch :: () -> s8 #foreign ncurses; - endwin :: () -> void #foreign ncurses; - - curs_set :: (visibility: s32) -> s32 #foreign ncurses; - mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; - noecho :: () -> s32 #foreign ncurses; - box :: (win :*WINDOW, verch :u8, horch :u8) -> s32 #foreign ncurses; - stdscr := initscr(); curs_set(0); noecho(); @@ -1921,3 +1863,4 @@ WINDOW :: struct { endwin(); } +*/ -- cgit v1.2.3 From 79dd6b6fcb349eae92c53bb3684015acb64fc852 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 27 Jan 2023 17:44:38 +0000 Subject: Processing version option argument. --- ttt.jai | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index c77f2a3..655fac8 100644 --- a/ttt.jai +++ b/ttt.jai @@ -21,6 +21,7 @@ // #import "File_Utilities"; VERSION :: "2.0"; // Use only 3 chars (to fit layouts). +YEAR :: "2023"; TASK_NAME_LENGTH :: 57; TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; // TODO Get rid of this! FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). @@ -1330,17 +1331,14 @@ main :: () { return; } - }}// TODO To be removed once we continue our journey... - /* + if is_equal_to_any(args[it], "--version", "-v") { - print("Task Time Tracker version % \n" - "Copyright 2023 Daniel Martins\n" - "License GPL-3.0-or-later\n" - ); + print("Task Time Tracker version % \nCopyright % Daniel Martins\nLicense GPL-3.0-or-later\n", VERSION, YEAR); free_memory(); - return EXIT_SUCCESS; + return; } - + }}// TODO To be removed once we continue our journey... + /* if (is_equal_to_any(args[it], "--import-csv", "-i")) { idx++; if (idx >= argc) { -- cgit v1.2.3 From 0063d59f4e073649c34e5d4c5a1341cd094f356e Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 28 Jan 2023 02:43:15 +0000 Subject: Simplified memory deallocation. Implemented argument parsing. Created prototype for app directory initialization. --- ttt.jai | 305 +++++++++++++++++++++++++++++----------------------------------- 1 file changed, 138 insertions(+), 167 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 655fac8..3be8718 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,8 +17,10 @@ #import "Basic"; #import "System"; #import "Math"; +#import "File"; +#import "String"; #import "curses"; -// #import "File_Utilities"; + VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2023"; @@ -54,71 +56,64 @@ Database :: struct { -/* -const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; -const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; -const size_t SIZEOF_TASK_ST = sizeof(task_st); -const size_t SIZEOF_DATABASE_ST = sizeof(database_st); -const size_t SIZEOF_CHAR = sizeof(char); -const size_t SIZEOF_INT64 = sizeof(int64_t); - - - -database_st database = { .tasks = NULL }; -database_st archive = { .tasks = NULL }; -database_st *db = NULL; -bool is_autosave_enabled = true; -int countdown_to_autosave = -1; -char *app_folder = NULL; -char *db_file_path = NULL; -char *ar_file_path = NULL; -char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors. -size_t string_buffer_size = 0; -int size_x, size_y, pos_x, pos_y; - - - -typedef enum { - STYLE_SELECTED = 1, - STYLE_SELECTED_INVERTED, - STYLE_ACTIVE, - STYLE_ACTIVE_SELECTED, - STYLE_ERROR, -} styles_et; - - - -WINDOW *error_window = NULL; -time_t error_time_limit = 0; - -void print_error(const char *format, ...) { - va_list args; - va_start(args, format); - if (stdscr == NULL || isendwin() == true) { // Not in ncurses mode? - vfprintf(stderr, format, args); - fprintf(stderr, "\n"); - } - else { - int w_size_x = size_x > 120 ? 120 : size_x - 2; - int w_size_y = 4; - if (error_window == NULL) { - error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); - wattron(error_window, COLOR_PAIR(STYLE_ERROR)); - wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); - mvwprintw(error_window, 0, 1, " Error "); - wmove(error_window, 1, 0); - } - else { - waddch(error_window, ' '); - } - vw_printw(error_window, format, args); - error_time_limit = time(NULL) + 5; - } - - va_end(args); +// const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; +// const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; +// const size_t SIZEOF_TASK_ST = sizeof(task_st); +// const size_t SIZEOF_DATABASE_ST = sizeof(database_st); +// const size_t SIZEOF_CHAR = sizeof(char); +// const size_t SIZEOF_INT64 = sizeof(int64_t); +// +// +// +// database_st database = { .tasks = NULL }; +// database_st archive = { .tasks = NULL }; +// database_st *db = NULL; +is_autosave_enabled := true; +// int countdown_to_autosave = -1; +app_directory : string; +db_file_path : string; +ar_file_path : string; +// char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors. +// size_t string_buffer_size = 0; +// int size_x, size_y, pos_x, pos_y; + +// typedef enum { +// STYLE_SELECTED = 1, +// STYLE_SELECTED_INVERTED, +// STYLE_ACTIVE, +// STYLE_ACTIVE_SELECTED, +// STYLE_ERROR, +// } styles_et; + +error_window : *WINDOW = null; +error_time_limit := Apollo_Time.{0, 0}; + +print_error :: (format :string, args : .. Any) { + // NOTE Implement me please. +// if stdscr == null || isendwin() == true { // Not in ncurses mode? + print(format, ..args); + print("\n"); +// } +// else { +// int w_size_x = size_x > 120 ? 120 : size_x - 2; +// int w_size_y = 4; +// if (error_window == NULL) { +// error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); +// wattron(error_window, COLOR_PAIR(STYLE_ERROR)); +// wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); +// mvwprintw(error_window, 0, 1, " Error "); +// wmove(error_window, 1, 0); +// } +// else { +// waddch(error_window, ' '); +// } +// vw_printw(error_window, format, args); +// error_time_limit = time(NULL) + 5; // NOTE Instead of time use the current_time_monotonic() +// } } -void draw_error_window() { +draw_error_window :: () { + /* NOTE Implement me please. if (error_window == NULL) { return; } @@ -144,8 +139,10 @@ void draw_error_window() { refresh(); touchwin(error_window); wrefresh(error_window); + */ } +/* void trigger_autosave() { countdown_to_autosave = 13375; // ms } @@ -1127,49 +1124,19 @@ void *mem_alloc(size_t mem_size, const char *error_tag) { */ free_memory :: () { + print(">> FREE <<\n"); /* TODO reset_database(&database); reset_database(&archive); free(string_buffer); string_buffer = NULL; - free(app_folder); app_folder = NULL; + free(app_directory); app_directory = NULL; free(db_file_path); db_file_path = NULL; free(ar_file_path); ar_file_path = NULL; */ } /* -bool initialize_app_folder() { - size_t temp_size; - - char *home_path = getenv(HOME_PATH_ENV); - if (home_path == NULL) { - home_path = "."; - } - temp_size = strlen(home_path) + 1 + strlen(APP_FOLDER_NAME) + 1; // Add space for folder separator and '\0'. - app_folder = mem_alloc(temp_size, "app folder"); - snprintf(app_folder, temp_size, "%s/%s", home_path, APP_FOLDER_NAME); - - // Create app folder. - mkdir(app_folder, 0740); - if (errno != 0 && errno != EEXIST) { - print_error("Failed to create app folder '%s': %s.", app_folder, strerror(errno)); - return false; - } - - // Set database file path. - temp_size = strlen(app_folder) + 1 + strlen(DB_FILE_NAME) + 1; // Add space for folder separator and '\0'. - db_file_path = mem_alloc(temp_size, "database file path"); - snprintf(db_file_path, temp_size, "%s/%s", app_folder, DB_FILE_NAME); - - // Set archive file path. - temp_size = strlen(app_folder) + 1 + strlen(AR_FILE_NAME) + 1; // Add space for folder separator and '\0'. - ar_file_path = mem_alloc(temp_size, "archive file path"); - snprintf(ar_file_path, temp_size, "%s/%s", app_folder, AR_FILE_NAME); - - return true; -} - void exit_gracefully(int signal) { flushinp(); ungetch('q'); @@ -1239,13 +1206,39 @@ bool read_enter_confirmation(int row, int style, const char *message) { main :: () { - /* - if (initialize_app_folder() == false) { - print_error("Failed to initialize app folder."); - free_memory(); - return EXIT_FAILURE; - } + defer free_memory(); + + { // Initialize app directory. + + home_dir, success := get_home_directory(); + if success == false { + home_dir = "."; + } + + app_directory, success = get_absolute_path(home_dir); // TODO Returned in the temporary allocator. + if success == false { + print_error("Failed to find home directory '%'.", home_dir); + return; + } + + app_directory = join(app_directory, "/", APP_FOLDER_NAME, allocator = temp); + // TODO DEBUG + // Check what's going on with the temp allocator: + // Is it really the responsible for these paths? + // It seems that the new compiler allows us to check this pretty easily. +// free(app_directory); + print("bazinga: '%'\n", app_directory); +// if make_directory_if_it_does_not_exist(app_directory, false) == false { +// print_error("Failed to create app directory '%'.", app_directory); +// return; +// } + db_file_path = join(app_directory, "/", DB_FILE_NAME, allocator = temp); + ar_file_path = join(app_directory, "/", AR_FILE_NAME, allocator = temp); + } + return; // TODO DEBUG + + /* db = &database; reset_database(&database); reset_database(&archive); @@ -1253,16 +1246,14 @@ main :: () { if (is_file_accessible(db_file_path) == false) { if (store_database(&database, db_file_path) == false) { print_error("Failed to initialize database."); - free_memory(); - return EXIT_FAILURE; + return; } } if (is_file_accessible(ar_file_path) == false) { if (export_to_csv(&archive, ar_file_path) == false) { print_error("Failed to initialize archive."); - free_memory(); - return EXIT_FAILURE; + return; } } */ @@ -1270,14 +1261,6 @@ main :: () { args := get_command_line_arguments(); defer array_reset(*args); - - - for args { - if it_index == 0 continue; - - - } - if args.count > 1 { is_exit_requested := false; @@ -1321,93 +1304,82 @@ main :: () { print(" END Select last/bottom task.\n"); print("\n"); print("Notes\n"); - print("- All data files are stored '%/.task_time_tracker'.\n", get_home_directory()); + print("- All data files are stored '%/.task_time_tracker'.\n", app_directory); print(" If the home directory is undefined, './.task_time_tracker' will be used.\n"); print(" The database entries are stored in binary format on the 'database.bin' file.\n"); print(" The archived entries are stored in CSV format on the 'archive.csv' file.\n"); print("- During intensive tasks such as saving to file or recalculating totals times,\n"); print(" a diamond symbol is shown on the top left corner.\n"); - free_memory(); return; } - 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); - free_memory(); return; } - }}// TODO To be removed once we continue our journey... - /* - if (is_equal_to_any(args[it], "--import-csv", "-i")) { - idx++; - if (idx >= argc) { + + if is_equal_to_any(args[it], "--import-csv", "-i") { + it += 1; + if it >= args.count { print_error("Missing CSV file path to import."); - free_memory(); - return EXIT_FAILURE; + return; } - if (load_database(&database, db_file_path) == false) { - print_error("Failed to load database."); - free_memory(); - return EXIT_FAILURE; - } - if (import_from_csv(&database, args[it]) == false) { - print_error("Failed to import CSV file."); - free_memory(); - return EXIT_FAILURE; - } - if (store_database(&database, db_file_path) == false) { - print_error("Failed to store database."); - free_memory(); - return EXIT_FAILURE; - } - reset_database(&database); + // TODO Implement +// if (load_database(&database, db_file_path) == false) { +// print_error("Failed to load database."); +// return; +// } +// if (import_from_csv(&database, args[it]) == false) { +// print_error("Failed to import CSV file."); +// return; +// } +// if (store_database(&database, db_file_path) == false) { +// print_error("Failed to store database."); +// return; +// } +// reset_database(&database); is_exit_requested = true; continue; } - if (is_equal_to_any(args[it], "--export-csv", "-e")) { - idx++; - if (idx >= argc) { + if is_equal_to_any(args[it], "--export-csv", "-e") { + it += 1; + if it >= args.count { print_error("Missing CSV file path to export."); - free_memory(); - return EXIT_FAILURE; - } - if (load_database(&database, db_file_path) == false) { - print_error("Failed to load database."); - free_memory(); - return EXIT_FAILURE; + return; } - if (export_to_csv(&database, args[it]) == false) { - print_error("Failed to export CSV file."); - free_memory(); - return EXIT_FAILURE; - } - reset_database(&database); + // TODO Implement +// if (load_database(&database, db_file_path) == false) { +// print_error("Failed to load database."); +// return; +// } +// if (export_to_csv(&database, args[it]) == false) { +// print_error("Failed to export CSV file."); +// return; +// } +// reset_database(&database); is_exit_requested = true; continue; } - if (is_equal_to_any(args[it], "--no-autosave", "-n")) { + if is_equal_to_any(args[it], "--no-autosave", "-n") { is_autosave_enabled = false; continue; } - print_error("%s: invalid option '%s'.\nTry '%s --help' for more information.", argv[0], args[it], argv[0]); - free_memory(); - return EXIT_FAILURE; + print_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]); + return; } - if (is_exit_requested) { - free_memory(); - return EXIT_SUCCESS; + if is_exit_requested { + return; } } + /* if (load_database(&database, db_file_path) == false) { print_error("Failed to load database."); - free_memory(); - return EXIT_FAILURE; + return; } initialize_tui(); @@ -1823,7 +1795,6 @@ main :: () { } endwin(); - free_memory(); return error_saving ? EXIT_FAILURE : EXIT_SUCCESS; */ } -- cgit v1.2.3 From f68ac18fa17de05df9be2d5f4eb991ebf844298f Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 28 Jan 2023 02:50:11 +0000 Subject: Added comments about memory owner. --- ttt.jai | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 3be8718..17de73b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1211,18 +1211,18 @@ main :: () { { // Initialize app directory. - home_dir, success := get_home_directory(); + home_dir, success := get_home_directory(); // Returns system owned memory. if success == false { home_dir = "."; } - app_directory, success = get_absolute_path(home_dir); // TODO Returned in the temporary allocator. + app_directory, success = get_absolute_path(home_dir); // TODO Returns temporary memory. if success == false { print_error("Failed to find home directory '%'.", home_dir); return; } - app_directory = join(app_directory, "/", APP_FOLDER_NAME, allocator = temp); + app_directory = join(app_directory, "/", APP_FOLDER_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. // TODO DEBUG // Check what's going on with the temp allocator: // Is it really the responsible for these paths? @@ -1233,8 +1233,8 @@ main :: () { // print_error("Failed to create app directory '%'.", app_directory); // return; // } - db_file_path = join(app_directory, "/", DB_FILE_NAME, allocator = temp); - ar_file_path = join(app_directory, "/", AR_FILE_NAME, allocator = temp); + db_file_path = join(app_directory, "/", DB_FILE_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. + ar_file_path = join(app_directory, "/", AR_FILE_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. } return; // TODO DEBUG -- cgit v1.2.3 From 114d85ad9c1d374e52deed138b3d624d3097b0c6 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 30 Jan 2023 18:04:25 +0000 Subject: Simplified help text printing. --- ttt.jai | 91 ++++++++++++++++++++++++++++++++++------------------------------- 1 file changed, 47 insertions(+), 44 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 17de73b..54cf640 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1266,50 +1266,53 @@ main :: () { is_exit_requested := false; for 1..args.count-1 { if is_equal_to_any(args[it], "--help", "-h") { - print("Usage: ttt [OPTION]... [FILE]...\n"); - print(" -i, --import-csv [FILE] Import CSV file to database (discard first row).\n"); - print(" -e, --export-csv [FILE] Export database to CSV file.\n"); - print(" -n, --no-autosave Disable autosave feature (only save on exit).\n"); - print(" -h, --help Display this help and exit.\n"); - print(" -v, --version Output version information and exit.\n"); - print("\n"); - print("In app commands\n"); - print(" a, A Archive selected task (except if active).\n"); - print(" r, R Restore selected task from archive.\n"); - print(" t, T Select currently active task (if any).\n"); - print(" d, D Duplicate selected task.\n"); - print(" n, N Create new task.\n"); - print(" m, M Move selected task to position.\n"); - print(" g, G Select task by position.\n"); - print(" q, Q Save changes and exit.\n"); - print(" F2 Rename selected task.\n"); - print(" F5 Recalculate total times.\n"); - print(" TAB Toggle archive view.\n"); - print(" BACKSPACE Reset times for selected task.\n"); - print(" DELETE Delete selected task (except if active).\n"); - print(" SPACE, ENTER Toggle selected task as active/inactive.\n"); - print(" 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n"); - print(" =# sets # seconds;\n"); - print(" -# subtracts # seconds;\n"); - print(" # adds # seconds;\n"); - print(" #m specifies # as minutes;\n"); - print(" #h specifies # as hours;\n"); - print(" #d specifies # as days;\n"); - print(" #y specifies # as years.\n"); - print(" UP Select task above.\n"); - print(" DOWN Select task below.\n"); - print(" PAGE-UP Select task 1 page above.\n"); - print(" PAGE-DOWN Select task 1 page below.\n"); - print(" HOME Select first/top task.\n"); - print(" END Select last/bottom task.\n"); - print("\n"); - print("Notes\n"); - print("- All data files are stored '%/.task_time_tracker'.\n", app_directory); - print(" If the home directory is undefined, './.task_time_tracker' will be used.\n"); - print(" The database entries are stored in binary format on the 'database.bin' file.\n"); - print(" The archived entries are stored in CSV format on the 'archive.csv' file.\n"); - print("- During intensive tasks such as saving to file or recalculating totals times,\n"); - print(" a diamond symbol is shown on the top left corner.\n"); + 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", + " -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", + " 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", + " n, N Create new task.\n", + " m, M Move selected task to position.\n", + " g, G Select task by position.\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 entries 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 tasks such as saving to file or recalculating totals times,\n", + " a diamond symbol is shown on the top left corner.\n" + ); return; } -- cgit v1.2.3 From 46a13cef3a7fa6f12b0a97a64d3a2250914fd445 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 1 Feb 2023 01:28:14 +0000 Subject: Added some code that may be usefull to check memory ownership on next compiler versions. --- ttt.jai | 59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 54cf640..5e5b2d7 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1226,7 +1226,64 @@ main :: () { // TODO DEBUG // Check what's going on with the temp allocator: // Is it really the responsible for these paths? - // It seems that the new compiler allows us to check this pretty easily. + // 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); + } + */ + // free(app_directory); print("bazinga: '%'\n", app_directory); // if make_directory_if_it_does_not_exist(app_directory, false) == false { -- cgit v1.2.3 From 99455454aea87534efe1a47694f2ff4274e9f7cd Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 1 Feb 2023 17:20:31 +0000 Subject: Added compilation commands in comments. --- ttt.jai | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 5e5b2d7..fc2e5a3 100644 --- a/ttt.jai +++ b/ttt.jai @@ -13,6 +13,9 @@ // You should have received a copy of the GNU General Public License along with // this program. If not, see . +// Compilation commands: +// - release : jai ttt.jai -import_dir . -x64 -release +// - debug : jai ttt.jai -import_dir . -x64 #import "Basic"; #import "System"; -- cgit v1.2.3 From 027f9648e00ba40c1e64da0827c93299106503dc Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 10 Feb 2023 19:35:41 +0000 Subject: Temporary code to check for memory owners. --- ttt.jai | 82 +++++++++++++++++--------------------------------------------- unused.jai | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 61 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index fc2e5a3..345e3cc 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1226,68 +1226,30 @@ main :: () { } app_directory = join(app_directory, "/", APP_FOLDER_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. - // TODO DEBUG - // 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); - } +// print(">%:%:%\n", temp, temp.data, context.temporary_storage.data); + print("#%\n", <%:%\n", app_directory.count, *app_directory.data); +// free(app_directory); + + /* WIP TEST THIS +some_pointer : *void = get_something(); +is_it_mine : bool = xx allocator.allocator_proc(.IS_THIS_YOURS, 0, 0, some_pointer, null); */ -// free(app_directory); + + reset_temporary_storage(); + a := join("wow", "testsesesrserse", "2222222222222222"); + b := join("wow", "testsesesrserse", "2222222222222222"); + c := join("wow", "testsesesrserse", "2222222222222222"); + d := join("wow", "testsesesrserse", "2222222222222222"); print("bazinga: '%'\n", app_directory); // if make_directory_if_it_does_not_exist(app_directory, false) == false { // print_error("Failed to create app directory '%'.", app_directory); diff --git a/unused.jai b/unused.jai index 3529a46..04997c5 100644 --- a/unused.jai +++ b/unused.jai @@ -1,3 +1,71 @@ + + + +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // + + +// 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; @@ -47,4 +115,3 @@ checked_sub :: (a: $T, b: T) -> result: T, overflow: bool return result, overflow; } - -- cgit v1.2.3 From 0b0559e3750c0cc0662640f293cfa7c1f8d0af17 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 16 Feb 2023 18:08:43 +0000 Subject: Initialize app directory and file paths. --- ttt.jai | 61 +++++++++++++++++++++++-------------------------------------- 1 file changed, 23 insertions(+), 38 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 345e3cc..1eaf8e1 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1207,56 +1207,31 @@ bool read_enter_confirmation(int row, int style, const char *message) { } */ - main :: () { defer free_memory(); - { // Initialize app directory. - - home_dir, success := get_home_directory(); // Returns system owned memory. - if success == false { + { // Initialize app directory. + home_dir, success_dir := get_home_directory(); // Returns system owned memory. + if success_dir == false { home_dir = "."; } - app_directory, success = get_absolute_path(home_dir); // TODO Returns temporary memory. - if success == false { + home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. + if success_path == false { print_error("Failed to find home directory '%'.", home_dir); return; } - app_directory = join(app_directory, "/", APP_FOLDER_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. -// print(">%:%:%\n", temp, temp.data, context.temporary_storage.data); - print("#%\n", <%:%\n", app_directory.count, *app_directory.data); -// free(app_directory); - - /* WIP TEST THIS -some_pointer : *void = get_something(); -is_it_mine : bool = xx allocator.allocator_proc(.IS_THIS_YOURS, 0, 0, some_pointer, null); - */ + 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); - - reset_temporary_storage(); - a := join("wow", "testsesesrserse", "2222222222222222"); - b := join("wow", "testsesesrserse", "2222222222222222"); - c := join("wow", "testsesesrserse", "2222222222222222"); - d := join("wow", "testsesesrserse", "2222222222222222"); - print("bazinga: '%'\n", app_directory); -// if make_directory_if_it_does_not_exist(app_directory, false) == false { -// print_error("Failed to create app directory '%'.", app_directory); -// return; -// } - db_file_path = join(app_directory, "/", DB_FILE_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. - ar_file_path = join(app_directory, "/", AR_FILE_NAME, allocator = temp); // TODO Change to default allocator because we want to keep this during runtime. + // TODO DEBUG + print_owner_allocator("home_path", home_path.data); + print_owner_allocator("app_directory", app_directory.data); + print_owner_allocator("db_file_path", db_file_path.data); + print_owner_allocator("ar_file_path", ar_file_path.data); } return; // TODO DEBUG @@ -1858,3 +1833,13 @@ main :: () { } */ + + +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); +} -- cgit v1.2.3 From d4d373de28e94e78e44a4f849dbae4d70f48eef9 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 17 Feb 2023 16:52:35 +0000 Subject: Prototypes for store and load database. --- ttt.jai | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 176 insertions(+), 11 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 1eaf8e1..585d7af 100644 --- a/ttt.jai +++ b/ttt.jai @@ -32,7 +32,7 @@ TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; // TODO Get rid of this! FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). NUM_WEEK_DAYS :: 7; // Just to be more clear about what we're looping about. -APP_FOLDER_NAME :: ".task_time_tracker"; +APP_FOLDER_NAME :: ".task_time_tracker_v2"; // TODO Using _v2 to avoid erasing my work data. DB_FILE_NAME :: "database.bin"; AR_FILE_NAME :: "archive.csv"; @@ -43,22 +43,26 @@ 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 { +OldTask :: struct { times : [NUM_WEEK_DAYS] s64; name : [TASK_NAME_BYTES] u8; // TODO Start using strings. } +Task :: struct { + times : [NUM_WEEK_DAYS] s64; + name : string~s8; + name_data : [62]u8; + name.data = cast(*~s8 u8) (0x80 ^ 0x01); // *name_data[0]; +} + Database :: struct { active_task : *~s64 Task = null; selected_task : *~s64 Task = null; - tasks : [..] Task; modified_on : s64; total_times : [NUM_WEEK_DAYS] s64; -// size_t count; // Will always be equal or less than capacity. TODO Will this be necessary? + tasks : [..] Task; } - - // const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; // const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; // const size_t SIZEOF_TASK_ST = sizeof(task_st); @@ -1226,13 +1230,174 @@ 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); + } + + + { // Load database prototype + +/* +typedef struct { + task_st *tasks; + size_t count; // Will always be equal or less than capacity. + size_t capacity; // Will always be equal or less than PTRDIFF_MAX (see MAX_DATABASE_TASKS). + ptrdiff_t active_task; // Will always be less than capacity/count. + ptrdiff_t selected_task; // Will always be less than capacity/count. + int64_t modified_on; + int64_t total_times[NUM_WEEK_DAYS]; +} database_st; +*/ +/* +Database :: struct { + active_task : *~s64 Task = null; + selected_task : *~s64 Task = null; + modified_on : s64; + total_times : [NUM_WEEK_DAYS] s64; + tasks : [..] Task; +} +*/ + + db : Database; + + // I'm having some troubles with the relative pointers: + // - array_add uses = to copy the pointers and I lose their values. + // - when printing the database after loading from file I'm getting a segmentation fault. + // also... the array_add + // TODO WIP + + //load_database(*db, db_file_path); + + task : Task; + memcpy(task.name.data, "bazinga".data, 7); + task.name.count = 7; + print(">>>'%'\n", task.name); + print("###%\n\n", task); + + add_task(*db, *task); + store_database(db, db_file_path); + + //array_add(*db.tasks, task); + //memcpy(*db.tasks[0], *task, size_of(Task)); + + //print("www\n"); + print("loaded: %\n", db); + + + + add_task :: (db: *Database, task: *Task) { + idx := db.tasks.count; + maybe_grow(*db.tasks); + db.tasks.count += 1; + memcpy(*db.tasks[idx], task, size_of(Task)); + } + - // TODO DEBUG - print_owner_allocator("home_path", home_path.data); - print_owner_allocator("app_directory", app_directory.data); - print_owner_allocator("db_file_path", db_file_path.data); - print_owner_allocator("ar_file_path", ar_file_path.data); + // Stores data from database into binary file. + // Returns success. + store_database :: (db: Database, path: string) -> success: bool { + //assert(db != null, "Parameter 'db' is null."); + assert(xx path, "Parameter 'path' is empty."); + + // Open file. + file, open_success := file_open(path, for_writing = true); // log_errors: bool = true + if open_success == false { + print_error("Failed to open file '%' while storing database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! + return false; + } + + file_write(*file, DB_FILE_SIGN_STR); + file_write(*file, *db, size_of(Database)); + //fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); + //fwrite(db, SIZEOF_DATABASE_ST, 1, file); + //fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file); + + file_close(*file); + return true; + } + + // Loads data from binary file into database. + // Returns success. + load_database :: (db: *Database, path: string) -> success: bool { + assert(db != null, "Parameter 'db' is null."); + assert(xx path, "Parameter 'path' is empty."); + + // Open file. + file, open_success := file_open(path); // log_errors: bool = true + if open_success == false { + print_error("Failed to open file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! + return false; + } + + // 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 print_error("Failed to read file signature from '%'.", path); + if cast(string)file_signature != DB_FILE_SIGN_STR { + print_error("Invalid file signature."); + file_close(*file); + return false; + } + + // Read database structure. + + read_success = file_read(file, db, size_of(Database)); + //if read_success == false print_error("Failed to read database info from '%'.", path); + + + //file_open :: (name: string, for_writing := false, keep_existing_content := false, log_errors := true) -> File, bool + //file_read :: (f: File, vdata: *void, bytes_to_read: s64) -> (success: bool, total_read: s64) + + return true; + } + + // Loads data from binary file into database. + // Returns success. + /* + load_database :: ( + + + bool load_database(database_st *db, const char *path) { + assert(db != NULL); + assert(path != NULL); + + // Open file. + FILE *file = fopen(path, "rb"); + if (file == NULL) { + print_error("Failed to open file '%s' while loading database: %s.", path, strerror(errno)); + return false; + } + + // Validate file signature. + char file_signature[DB_FILE_SIGN_LENGTH]; + fread(&file_signature, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); + if (strncmp(file_signature, DB_FILE_SIGN, DB_FILE_SIGN_LENGTH) != 0) { + print_error("Invalid file signature."); + fclose(file); + return false; + } + + // Read database structure. + fread(db, SIZEOF_DATABASE_ST, 1, file); + + // Reserve database capacity for tasks. + size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; + db->tasks = malloc(capacity_bytes); + if (db->tasks == NULL && capacity_bytes > 0) { + print_error("Failed to allocate memory while loading database: %s.", strerror(errno)); + return false; + } + + // Read database tasks. + fread(db->tasks, SIZEOF_TASK_ST, db->count, file); + + // Make sure we are reading all the file. + assert(fgetc(file) == EOF); + + fclose(file); + return true; + } + */ } + return; // TODO DEBUG /* -- cgit v1.2.3 From 1c4f10d917b8bdff3c2b066f4b51843d4ad490b7 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 18 Feb 2023 02:12:02 +0000 Subject: First functional prototype of database load and store. --- ttt.jai | 67 ++++++++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 24 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 585d7af..584548a 100644 --- a/ttt.jai +++ b/ttt.jai @@ -55,11 +55,15 @@ Task :: struct { name.data = cast(*~s8 u8) (0x80 ^ 0x01); // *name_data[0]; } -Database :: struct { - active_task : *~s64 Task = null; - selected_task : *~s64 Task = null; +DatabaseCore :: struct { + active_idx : s64; + selected_task : s64; modified_on : s64; total_times : [NUM_WEEK_DAYS] s64; +} + +Database :: struct { + #as using base : DatabaseCore; tasks : [..] Task; } @@ -1264,24 +1268,40 @@ Database :: struct { // also... the array_add // TODO WIP - //load_database(*db, db_file_path); - - task : Task; - memcpy(task.name.data, "bazinga".data, 7); - task.name.count = 7; - print(">>>'%'\n", task.name); - print("###%\n\n", task); + load_database(*db, db_file_path); - add_task(*db, *task); - store_database(db, db_file_path); +// task : Task; +// memcpy(task.name.data, "bazinga".data, 7); +// task.name.count = 7; +// { +// path := join(app_directory, "/", "test"); +// file := file_open(path, for_writing = true); +// file_write(*file, *task, size_of(Task)); +// file_close(*file); +// +// data: Task; +// file = file_open(path); +// file_read(file, *data, size_of(Task)); +// print("### > %\n", data); +// } +// array_add(*db.tasks, task); +// store_database(db, db_file_path); - //array_add(*db.tasks, task); - //memcpy(*db.tasks[0], *task, size_of(Task)); +// print("size_of([..]) = %\n", size_of([..] Task)); //print("www\n"); print("loaded: %\n", db); +// print(">>>'%'\n", task.name); +// print("###%\n\n", task); +// print(":%\n", size_of(type_of(db.tasks))); - + array_add :: (array: *[..] $T/Task, item: T) #no_abc { + print("my_array\n"); + maybe_grow(array); +// array.data[array.count] = item; + memcpy(*array.data[array.count], *item, size_of(T)); + array.count += 1; + } add_task :: (db: *Database, task: *Task) { idx := db.tasks.count; @@ -1290,7 +1310,6 @@ Database :: struct { memcpy(*db.tasks[idx], task, size_of(Task)); } - // Stores data from database into binary file. // Returns success. store_database :: (db: Database, path: string) -> success: bool { @@ -1305,7 +1324,9 @@ Database :: struct { } file_write(*file, DB_FILE_SIGN_STR); - file_write(*file, *db, size_of(Database)); + file_write(*file, *db, size_of(DatabaseCore)); + file_write(*file, *db.tasks.count, size_of(type_of(Database.tasks.count))); + file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); //fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); //fwrite(db, SIZEOF_DATABASE_ST, 1, file); //fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file); @@ -1338,13 +1359,11 @@ Database :: struct { } // Read database structure. - - read_success = file_read(file, db, size_of(Database)); - //if read_success == false print_error("Failed to read database info from '%'.", path); - - - //file_open :: (name: string, for_writing := false, keep_existing_content := false, log_errors := true) -> File, bool - //file_read :: (f: File, vdata: *void, bytes_to_read: s64) -> (success: bool, total_read: s64) + read_success = file_read(file, db, size_of(DatabaseCore)); + tasks_count: s64; + file_read(file, *tasks_count, size_of(type_of(Database.tasks.count))); + array_resize(*db.tasks, tasks_count, initialize = false); + file_read(file, db.tasks.data, size_of(Task) * tasks_count); return true; } -- cgit v1.2.3 From b75c965d3d2bb4bd9059d0df0366cd54f76825ff Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 26 Feb 2023 03:06:09 +0000 Subject: Prototyping syscalls. --- syscall.jai | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ttt.jai | 16 ++++++++------ 2 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 syscall.jai (limited to 'ttt.jai') diff --git a/syscall.jai b/syscall.jai new file mode 100644 index 0000000..46a9d84 --- /dev/null +++ b/syscall.jai @@ -0,0 +1,69 @@ +#import "Basic"; +#import "POSIX"; + +// This prototype is suposed to allow me to use the "open" posix syscall +// that returns a file-descriptor (fd) that can then be used by the mmap +// syscall that allows to map a file to memory. +// +// Syscall info: https://filippo.io/linux-syscall-table/ + +main :: () { + print("starting syscall\n"); + + sys_exit(-1); + fd := sys_open("./dummy"); + print("fd is %\nerrno is %\n", fd, errno()); + + print("done syscall\n"); + LOOPER :: "\\|/-"; + idx := 0; + loop: string; + loop.count = 1; + for 0..3 { + sleep_milliseconds(1000); + loop.data = *LOOPER.data[idx]; + idx = (idx + 1) % LOOPER.count; + print("\rlooping %", loop); + } + + sys_close(fd); + print("\rclosed\n"); + + for 0..3 { + sleep_milliseconds(1000); + loop.data = *LOOPER.data[idx]; + idx = (idx + 1) % LOOPER.count; + print("\rlooping %", loop); + } +} + +sys_open :: (path: string) -> fd: s64 { + SYS_OPEN :: 2; + return syscall(SYS_OPEN, cast(*s8)path.data, O_RDONLY); +} + +sys_close :: (fd: s64) { + SYS_CLOSE :: 3; + syscall(SYS_CLOSE, fd); +} + +sys_exit :: (status: s32) { +#if OS == .WINDOWS { + // @Note TerminateProcess may be better, as it can't deadlock + ExitProcess :: (error_code: u32) #foreign kernel32; + ExitProcess(xx errno); +} +else #if OS == .LINUX { + SYS_EXIT_GROUP :: 231; + #if CPU==.X64 { + print("\rLINUX-ASM\n"); + #asm SYSCALL_SYSRET { + mov eax: gpr === a, SYS_EXIT_GROUP; + mov out: gpr === di, status; + syscall t1:, t2:, eax, out; + }; + } else { + syscall(SYS_EXIT_GROUP, status); + } +} +} diff --git a/ttt.jai b/ttt.jai index 584548a..e69132e 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1250,15 +1250,19 @@ typedef struct { int64_t total_times[NUM_WEEK_DAYS]; } database_st; */ -/* -Database :: struct { - active_task : *~s64 Task = null; - selected_task : *~s64 Task = null; + +DatabaseX :: struct { + active_idx : s64; + selected_task : s64; modified_on : s64; total_times : [NUM_WEEK_DAYS] s64; - tasks : [..] Task; + tasks : [] Task; } -*/ + + + dd: DatabaseX; + + db : Database; -- cgit v1.2.3 From ffbcc2d1556dcd55f53abb320c5be72951506f9f Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 26 Feb 2023 18:24:30 +0000 Subject: Prototype implementation of import_csv, sys_open, sys_close, and usage of map_entire_file_start. --- syscall.jai | 107 +++++++++++++++++++----- ttt.jai | 274 +++++++++++++++++++++++++++++------------------------------- 2 files changed, 216 insertions(+), 165 deletions(-) (limited to 'ttt.jai') diff --git a/syscall.jai b/syscall.jai index 46a9d84..e30dc2f 100644 --- a/syscall.jai +++ b/syscall.jai @@ -8,43 +8,106 @@ // Syscall info: https://filippo.io/linux-syscall-table/ main :: () { - print("starting syscall\n"); - - sys_exit(-1); - fd := sys_open("./dummy"); - print("fd is %\nerrno is %\n", fd, errno()); - print("done syscall\n"); + //sys_exit(-1); + print("> open dummy\n"); + fd, error_open := sys_open("./dummy"); + print("> opened dummy with fd % and errno %\n", fd, error_open); + if fd < 0 return; + LOOPER :: "\\|/-"; idx := 0; loop: string; loop.count = 1; - for 0..3 { + for 0..1 { sleep_milliseconds(1000); loop.data = *LOOPER.data[idx]; idx = (idx + 1) % LOOPER.count; print("\rlooping %", loop); } + + error_close := sys_close(fd); + print("\r> close(%) with errno %\n", fd, error_close); - sys_close(fd); - print("\rclosed\n"); - - for 0..3 { - sleep_milliseconds(1000); - loop.data = *LOOPER.data[idx]; - idx = (idx + 1) % LOOPER.count; - print("\rlooping %", loop); - } + print("---\n"); + error :s64; + fd, error = sys_open("./fail"); + print("$ open(fail) returned fd % and error %\n", fd, error); + error = sys_close(456); + print("$ close(456) returned error %\n", error); } -sys_open :: (path: string) -> fd: s64 { - SYS_OPEN :: 2; - return syscall(SYS_OPEN, cast(*s8)path.data, O_RDONLY); + + + + + + + +sys_open :: (path: string) -> file_descriptor: s64, error: s64 { + + #if OS == .WINDOWS { + print("TODO Implement this\n"); // TODO Implement this. + } + else #if OS == .LINUX { + SYS_OPEN :: 2; + data: *s8 = xx path.data; + result: s64 = ---; + #if CPU == .X64 { + print("\r# LINUX:ASM\n"); + #asm SYSCALL_SYSRET { + mov rax: gpr === a, SYS_OPEN; + mov rdi: gpr === di, data; + mov rsi: gpr === si, O_RDONLY; + mov rdx: gpr === d, O_RDONLY; + //syscall rcx, r11, rax, rdi, rsi, rdx; // TODO Cleanup? + syscall + rcx: gpr === c, // Implicitly used by syscall to store RIP + r11: gpr === 11, // Implicitly used by syscall to store RFLAGS + rax, rdi, rsi, rdx; + mov result, rax; + } + if result < 0 + return -1, -result; // Error. + else + return result, 0; // OK. + } else { + print("\r# LINUX:LIBC\n"); + result = syscall(SYS_OPEN, data, O_RDONLY); + if result < 0 + return -1, errno(); // Error. + else + return result, 0; // OK. + } + } } -sys_close :: (fd: s64) { - SYS_CLOSE :: 3; - syscall(SYS_CLOSE, fd); +sys_close :: (file_descriptor: s64) -> error: s64 { + + #if OS == .WINDOWS { + print("TODO Implement this\n"); // TODO Implement this. + } + else #if OS == .LINUX { + SYS_CLOSE :: 3; + result: s64 = ---; + #if CPU == .X64 { + print("\r# LINUX:ASM\n"); + #asm SYSCALL_SYSRET { + mov rax: gpr === a, SYS_CLOSE; + mov rdi: gpr === di, file_descriptor; + syscall + rcx: gpr === c, // Implicitly used by syscall to store RIP + r11: gpr === 11, // Implicitly used by syscall to store RFLAGS + rax, rdi; + mov result, rax; + } + return ifx result < 0 then -result else 0; + } else { + print("\r# LINUX:LIBC\n"); + result = syscall(SYS_CLOSE, file_descriptor); + return ifx result < 0 then errno() else 0; + } + } } sys_exit :: (status: s32) { diff --git a/ttt.jai b/ttt.jai index e69132e..9592d5b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -43,27 +43,18 @@ SECONDS_IN_DAY :: cast(s64)24*SECONDS_IN_HOUR; SECONDS_IN_YEAR :: cast(s64)365*SECONDS_IN_DAY; MAX_DATABASE_TASKS :: S64_MAX; -OldTask :: struct { - times : [NUM_WEEK_DAYS] s64; - name : [TASK_NAME_BYTES] u8; // TODO Start using strings. -} - Task :: struct { times : [NUM_WEEK_DAYS] s64; - name : string~s8; - name_data : [62]u8; - name.data = cast(*~s8 u8) (0x80 ^ 0x01); // *name_data[0]; + name : [72] u8; + //name_data : [70] u8; + //name.data = cast(*~s8 u8) (0x80 ^ 0x01); // *name_data[0]; } -DatabaseCore :: struct { +Database :: struct { + modified_on : Apollo_Time; active_idx : s64; - selected_task : s64; - modified_on : s64; + selected_idx : s64; total_times : [NUM_WEEK_DAYS] s64; -} - -Database :: struct { - #as using base : DatabaseCore; tasks : [..] Task; } @@ -325,7 +316,18 @@ task_st *get_selected_task(database_st *db) { } return task; } - +*/ +// Creates new task stored at location given by task pointer. +// If necessary, expands database capacity. +// Returns success. +add_task :: (db: *Database, task: *Task) -> success: bool { + idx := db.tasks.count; + maybe_grow(*db.tasks); // TODO Check for errors? + db.tasks.count += 1; + db.tasks[idx] = task; + return true; +} +/* // Creates new task stored at location given by task pointer. // If necessary, expands database capacity. // Returns success. @@ -693,70 +695,83 @@ bool export_to_csv(const database_st *db, const char *path) { fclose(file); return true; } - +*/ // Imports CSV file into database. // Returns success. -bool import_from_csv(database_st *db, const char *path) { - assert(db != NULL); - assert(path != NULL); +import_from_csv :: (db: *Database, path: string) -> bool { - FILE *file = fopen(path, "r"); - if (file == NULL) { - print_error("Failed to open file '%s' while importing from CSV: %s.", path, strerror(errno)); + assert(db != null, "Parameter 'db' is null."); + assert(xx path, "Parameter 'path' is empty."); + + // Read file. + file_data, success := read_entire_file(path); // TODO Read entire file... may fail it file is too big. + if success == false { + print_error("Failed to read file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } + defer free(file_data.data); + csv := file_data; - // Skip header line. - fscanf(file, "%*[^\n]\n"); + // TODO Helper function. + consume_next_line :: (sp: *string) -> string, bool { + // To find the end of the line, we look for a linefeed character. + // We will trim a carriage return off the end if there is one there also. + // Thus this works on both 'dos' and 'unix'-style files. - // Parse CSV file. - char *csv_buffer = NULL; - size_t csv_buffer_size = 0; - while(getline(&csv_buffer, &csv_buffer_size, file) != -1) { // Check if reached EOF. - - // Find task name string limits. - char *name_delimiter = strchr(csv_buffer, ','); - if (name_delimiter == NULL) { - continue; - } - size_t name_length = (name_delimiter - csv_buffer); - if (name_length > TASK_NAME_LENGTH) { - name_length = TASK_NAME_LENGTH; + s := << sp; + found, result, right := split_from_left(s, 10); + + if !found { + // This is the last line; there may not have been a linefeed after that, + // but we still want to handle that data, so we return true if there was + // a nonzero amount of stuff there. + + << sp = ""; + + return s, (s.count > 0); } - - // Prepare new task. - task_st *task; - if (create_task(db, &task) == false) { - return false; + + // Chop the characters we are going to return from 'sp', + // which holds the remaining file data. + advance(sp, result.count + 1); + + if result { + if result[result.count-1] == 13 result.count -= 1; // If there's a carriage return at the end, remove it by decrementing the string's length. } - // Import task name. - memcpy(task->name, csv_buffer, name_length); - truncate_string_utf8(task->name, name_length); + return result, true; + } + + //Skip header line. + consume_next_line(*csv); + + // TODO Helper function. + advance :: inline (array: *[] $T, amount: int = 1) { + assert(amount >= 0); + assert(array.count >= amount); + array.count -= amount; + array.data += amount; + } + + // Parse CSV lines. + while (true) { + line, success := consume_next_line(*csv); + if success == false break; - // Parse task times. - if (sscanf(name_delimiter + 1, - "%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64, - &task->times[0], &task->times[1], &task->times[2], &task->times[3], &task->times[4], &task->times[5], &task->times[6] - ) != NUM_WEEK_DAYS - ) { - replace_char(csv_buffer, '\n', ' '); - print_error("Discarding invalid line '%s' and continuing.", csv_buffer); - delete_task(db, task); - continue; + task: Task; + values := split(line, ","); // TODO Temporary memory... if file is too big this may break. + memcpy(task.name.data, values[0].data, xx min(task.name.count, values[0].count)); + advance(*values); + for values { + task.times[it_index] = string_to_int(it); } - // Add task timer values to total timers. - for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { - db->total_times[idx] = add_int64(db->total_times[idx], task->times[idx]); - } + add_task(db, *task); } - fclose(file); - free(csv_buffer); - return true; + return false; } - +/* // Appends task to the end of the CSV file. // Returns success. bool append_to_csv(task_st *task, const char *path) { @@ -1237,82 +1252,53 @@ main :: () { } - { // Load database prototype - -/* -typedef struct { - task_st *tasks; - size_t count; // Will always be equal or less than capacity. - size_t capacity; // Will always be equal or less than PTRDIFF_MAX (see MAX_DATABASE_TASKS). - ptrdiff_t active_task; // Will always be less than capacity/count. - ptrdiff_t selected_task; // Will always be less than capacity/count. - int64_t modified_on; - int64_t total_times[NUM_WEEK_DAYS]; -} database_st; -*/ - -DatabaseX :: struct { - active_idx : s64; - selected_task : s64; - modified_on : s64; - total_times : [NUM_WEEK_DAYS] s64; - tasks : [] Task; -} - - - dd: DatabaseX; - - - - db : Database; - - // I'm having some troubles with the relative pointers: - // - array_add uses = to copy the pointers and I lose their values. - // - when printing the database after loading from file I'm getting a segmentation fault. - // also... the array_add - // TODO WIP - - load_database(*db, db_file_path); + + { // Try out the mapping file in linux. + //a, b := file_open(db_file_path); + //c, d := file_open(ar_file_path); + mfi, success := map_entire_file_start(ar_file_path); + print("Success is %\n", success); + print("MFI is %\n", mfi); + print("MFI.data.count is %\n", mfi.data.count); + print("MFI.data.data[1] is %\n", mfi.data.data[123]); + print("%\n", ifx success then "success" else "fail"); + print("--------------------------------------\n"); -// task : Task; -// memcpy(task.name.data, "bazinga".data, 7); -// task.name.count = 7; -// { -// path := join(app_directory, "/", "test"); -// file := file_open(path, for_writing = true); -// file_write(*file, *task, size_of(Task)); -// file_close(*file); -// -// data: Task; -// file = file_open(path); -// file_read(file, *data, size_of(Task)); -// print("### > %\n", data); -// } -// array_add(*db.tasks, task); -// store_database(db, db_file_path); + print(">>>%\n", mfi.map_info.file.handle.unknown_pre[111]); + print("###%\n", mfi.map_info.file.handle._file); + //file_h := c.handle; + //for file_h.cena + //if it == 4 { + //data := file_h.cena.data; + //print("HERE[%] = %\n", it_index-4, data[it-4]); + //print("HERE[%] = %\n", it_index-3, data[it-3]); + //print("HERE[%] = %\n", it_index-2, data[it-2]); + //print("HERE[%] = %\n", it_index-1, data[it-1]); + //print("HERE[%] = %\n", it_index, it); + //print("HERE[%] = %\n", it_index+1, data[it+1]); + //print("HERE[%] = %\n", it_index+2, data[it+2]); + //print("HERE[%] = %\n", it_index+3, data[it+3]); + //print("HERE[%] = %\n", it_index+4, data[it+4]); + //} + //print("%\n", <>>'%'\n", task.name); -// print("###%\n\n", task); -// print(":%\n", size_of(type_of(db.tasks))); + import_from_csv(*db, ar_file_path); + + for db.tasks print_task(it); - array_add :: (array: *[..] $T/Task, item: T) #no_abc { - print("my_array\n"); - maybe_grow(array); -// array.data[array.count] = item; - memcpy(*array.data[array.count], *item, size_of(T)); - array.count += 1; + print_task :: (task: Task) { + for task.times print("% |", it); + print(" %\n", cast(string)task.name); } - add_task :: (db: *Database, task: *Task) { - idx := db.tasks.count; - maybe_grow(*db.tasks); - db.tasks.count += 1; - memcpy(*db.tasks[idx], task, size_of(Task)); - } // Stores data from database into binary file. // Returns success. @@ -1326,16 +1312,14 @@ DatabaseX :: struct { print_error("Failed to open file '%' while storing database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } + defer file_close(*file); file_write(*file, DB_FILE_SIGN_STR); - file_write(*file, *db, size_of(DatabaseCore)); - file_write(*file, *db.tasks.count, size_of(type_of(Database.tasks.count))); - file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); + file_write(*file, *db, size_of(Database)); //fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); //fwrite(db, SIZEOF_DATABASE_ST, 1, file); //fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file); - file_close(*file); return true; } @@ -1351,6 +1335,7 @@ DatabaseX :: struct { print_error("Failed to open file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } + defer file_close(*file); // Validate file signature. file_signature: [DB_FILE_SIGN_STR.count] u8; @@ -1363,15 +1348,18 @@ DatabaseX :: struct { } // Read database structure. - read_success = file_read(file, db, size_of(DatabaseCore)); - tasks_count: s64; - file_read(file, *tasks_count, size_of(type_of(Database.tasks.count))); - array_resize(*db.tasks, tasks_count, initialize = false); - file_read(file, db.tasks.data, size_of(Task) * tasks_count); + + read_success = file_read(file, db, size_of(Database)); + //if read_success == false print_error("Failed to read database info from '%'.", path); + + + //file_open :: (name: string, for_writing := false, keep_existing_content := false, log_errors := true) -> File, bool + //file_read :: (f: File, vdata: *void, bytes_to_read: s64) -> (success: bool, total_read: s64) return true; } + // Loads data from binary file into database. // Returns success. /* -- cgit v1.2.3 From 968ffd3966f5b23bb2b32cff803229732ae8bdb1 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 27 Feb 2023 03:16:54 +0000 Subject: Implemented prototypes for sys_stat and sys_fstat to replace lseek used to get file size. --- syscall.jai | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++---------- ttt.jai | 34 ++++++++++++++++--- 2 files changed, 119 insertions(+), 23 deletions(-) (limited to 'ttt.jai') diff --git a/syscall.jai b/syscall.jai index e5dee41..d04897c 100644 --- a/syscall.jai +++ b/syscall.jai @@ -10,7 +10,9 @@ main :: () { //sys_exit(-1); - print("> open dummy\n"); + size_stat, error_stat := sys_stat("./dummy"); + print("> dymmy has stat size % widh error %\n", size_stat.st_size, error_stat); + fd, error_open := sys_open("./dummy", O_RDONLY, O_RDONLY); print("> opened dummy with fd % and errno %\n", fd, error_open); if fd < 0 return; @@ -28,8 +30,11 @@ main :: () { //} - size, error_lseek := sys_lseek(fd, 0, SEEK_END); - print("\r> dummy has size % with error %\n", size, error_lseek); + size_lseek, error_lseek := sys_lseek(fd, 0, SEEK_END); + print("\r> dummy has lseek size % with error %\n", size_lseek, error_lseek); + + size_fstat, error_fstat := sys_fstat(fd); + print("\r> dummy has fstat size % with error %\n", size_fstat.st_size, error_fstat); error_close := sys_close(fd); print("\r> close(%) with errno %\n", fd, error_close); @@ -43,16 +48,83 @@ main :: () { } +sys_stat :: (path: string) -> result: stat_t, error: s64{ + print("\r# sys_stat\n"); + #if OS == .LINUX { + SYS_STAT :: 4; + stats: stat_t = ---; + result: s64 = ---; + #if CPU == .X64 { + print("\r# LINUX:ASM\n"); + path_p: *s8 = xx path.data; + stats_p := *stats; + #asm SYSCALL_SYSRET { + mov rax: gpr === a, SYS_STAT; + mov rdi: gpr === di, path_p; + mov rsi: gpr === si, stats_p; + syscall + rcx: gpr === c, // Implicitly used by syscall to store RIP + r11: gpr === 11, // Implicitly used by syscall to store RFLAGS + rax, rdi, rsi; + mov result, rax; + } + if result < 0 + return .{}, -result; // Error. + else + return stats, 0; // OK. + } else { + print("\r# LINUX:LIBC\n"); + path_p: *s8 = xx path.data; + error := syscall(SYS_STAT, path_p, *stats); + if error < 0 + return .{}, errno(); // Error. + else + return stats, 0; // OK. + } + } else { + print("TODO Implement this\n"); // TODO Implement this. + } +} - - +sys_fstat :: (file_descriptor: s64) -> result: stat_t, error: s64{ + print("\r# sys_fstat\n"); + #if OS == .LINUX { + SYS_FSTAT :: 5; + stats: stat_t = ---; + stats_p := *stats; + result: s64 = ---; + #if CPU == .X64 { + print("\r# LINUX:ASM\n"); + #asm SYSCALL_SYSRET { + mov rax: gpr === a, SYS_FSTAT; + mov rdi: gpr === di, file_descriptor; + mov rsi: gpr === si, stats_p; + syscall + rcx: gpr === c, // Implicitly used by syscall to store RIP + r11: gpr === 11, // Implicitly used by syscall to store RFLAGS + rax, rdi, rsi; + mov result, rax; + } + if result < 0 + return .{}, -result; // Error. + else + return stats, 0; // OK. + } else { + print("\r# LINUX:LIBC\n"); + error := syscall(SYS_FSTAT, file_descriptor, *result); + if error < 0 + return .{}, errno(); // Error. + else + return stats, 0; // OK. + } + } else { + print("TODO Implement this\n"); // TODO Implement this. + } +} sys_lseek :: (file_descriptor: s64, offset: s64, whence: s64) -> offset_location: s64, error: s64 { print("\r# sys_lseek\n"); - #if OS == .WINDOWS { - print("TODO Implement this\n"); // TODO Implement this. - } - else #if OS == .LINUX { + #if OS == .LINUX { SYS_LSEEK :: 8; result: s64 = ---; #if CPU == .X64 { @@ -81,15 +153,14 @@ sys_lseek :: (file_descriptor: s64, offset: s64, whence: s64) -> offset_location else return result, 0; // OK. } - } + } else { + print("TODO Implement this\n"); // TODO Implement this. + } } sys_open :: (path: string, flags: s64, mode: s64) -> file_descriptor: s64, error: s64 { print("\r# sys_open\n"); - #if OS == .WINDOWS { - print("TODO Implement this\n"); // TODO Implement this. - } - else #if OS == .LINUX { + #if OS == .LINUX { SYS_OPEN :: 2; data: *s8 = xx path.data; result: s64 = ---; @@ -119,15 +190,14 @@ sys_open :: (path: string, flags: s64, mode: s64) -> file_descriptor: s64, error else return result, 0; // OK. } + } else { + print("TODO Implement this\n"); // TODO Implement this. } } sys_close :: (file_descriptor: s64) -> error: s64 { print("\r# sys_close\n"); - #if OS == .WINDOWS { - print("TODO Implement this\n"); // TODO Implement this. - } - else #if OS == .LINUX { + #if OS == .LINUX { SYS_CLOSE :: 3; result: s64 = ---; #if CPU == .X64 { @@ -147,5 +217,7 @@ sys_close :: (file_descriptor: s64) -> error: s64 { result = syscall(SYS_CLOSE, file_descriptor); return ifx result < 0 then errno() else 0; } + } else { + print("TODO Implement this\n"); // TODO Implement this. } } diff --git a/ttt.jai b/ttt.jai index 9592d5b..7612f1b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1258,14 +1258,14 @@ main :: () { //c, d := file_open(ar_file_path); mfi, success := map_entire_file_start(ar_file_path); print("Success is %\n", success); - print("MFI is %\n", mfi); + //print("MFI is %\n", mfi); print("MFI.data.count is %\n", mfi.data.count); - print("MFI.data.data[1] is %\n", mfi.data.data[123]); + //print("MFI.data.data[1] is %\n", mfi.data.data[123]); print("%\n", ifx success then "success" else "fail"); print("--------------------------------------\n"); - - print(">>>%\n", mfi.map_info.file.handle.unknown_pre[111]); - print("###%\n", mfi.map_info.file.handle._file); + //print(">>>%\n", mfi.map_info.file.handle.unknown_pre[111]); + //print("###%\n", mfi.map_info.file.handle._file); + print("###%\n", mfi.map_info.file_descriptor); //file_h := c.handle; //for file_h.cena //if it == 4 { @@ -1282,6 +1282,30 @@ main :: () { //} //print("%\n", < IN LOOP <\n"); + peek :string; + peek.data = mfi.map_info.data.data; + peek.count = 1; + seek := 0; + while true { + //sleep_milliseconds(10); + print("peeking '%'\n", peek); + peek.data += 100000; + seek += 100000; + if seek >= (186880600/2) + break; + } + print("-- peek complete\n"); + print("MFI.data.count is %\n", mfi.data.count); + sleep_milliseconds(10000); + print("-- unloading..."); + map_entire_file_end(*mfi); + print("done\n"); + sleep_milliseconds(10000); + //print("-- reading entire file"); + //read_entire_file(ar_file_path); + //print("done\n"); + //sleep_milliseconds(10000); return; } -- cgit v1.2.3 From 056f874d7df4f243d873f6c045e926ee28988a10 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 8 Mar 2023 01:50:03 +0000 Subject: Prototyping import_from_csv. --- ttt.jai | 118 ++++++++++++++++++++++++++++------------------------------------ 1 file changed, 51 insertions(+), 67 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 7612f1b..392b692 100644 --- a/ttt.jai +++ b/ttt.jai @@ -20,6 +20,7 @@ #import "Basic"; #import "System"; #import "Math"; +#import "POSIX"; #import "File"; #import "String"; #import "curses"; @@ -700,17 +701,38 @@ bool export_to_csv(const database_st *db, const char *path) { // Returns success. import_from_csv :: (db: *Database, path: string) -> bool { + // TODO WIP + assert(db != null, "Parameter 'db' is null."); assert(xx path, "Parameter 'path' is empty."); + error_code: s64; + + // Check file size + file_info: stat_t; + error_code = sys_stat(path, *file_info); // TODO Check for error. + size := file_info.st_size; + + success: bool; + map: Map_File_Info; + data: string; + is_using_map := false; + if size >= 1<<30 { + print("big file with % MB\n", size>>20); // TODO + map, success = map_entire_file_start(path); + data = map.data; + is_using_map = true; + } + else { + print("small file with % B\n", size); // TODO + data, success = read_entire_file(path); + } + defer if is_using_map then map_entire_file_end(*map); else free(data.data); + csv := data; - // Read file. - file_data, success := read_entire_file(path); // TODO Read entire file... may fail it file is too big. if success == false { print_error("Failed to read file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } - defer free(file_data.data); - csv := file_data; // TODO Helper function. consume_next_line :: (sp: *string) -> string, bool { @@ -753,9 +775,24 @@ import_from_csv :: (db: *Database, path: string) -> bool { array.data += amount; } + next_line :: inline (csv: *string) -> line: string, success: bool { + for 0..csv.count { + if csv.data[it] == #char "\n" { + line: string = < bool { } add_task(db, *task); +// if context.temporary_storage.total_bytes_occupied > (100<<20) { +// print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); +// reset_temporary_storage(); +// } } - + print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); + reset_temporary_storage(); + return false; } /* @@ -1252,76 +1295,17 @@ main :: () { } - - { // Try out the mapping file in linux. - //a, b := file_open(db_file_path); - //c, d := file_open(ar_file_path); - mfi, success := map_entire_file_start(ar_file_path); - print("Success is %\n", success); - //print("MFI is %\n", mfi); - print("MFI.data.count is %\n", mfi.data.count); - //print("MFI.data.data[1] is %\n", mfi.data.data[123]); - print("%\n", ifx success then "success" else "fail"); - print("--------------------------------------\n"); - //print(">>>%\n", mfi.map_info.file.handle.unknown_pre[111]); - //print("###%\n", mfi.map_info.file.handle._file); - print("###%\n", mfi.map_info.file_descriptor); - //file_h := c.handle; - //for file_h.cena - //if it == 4 { - //data := file_h.cena.data; - //print("HERE[%] = %\n", it_index-4, data[it-4]); - //print("HERE[%] = %\n", it_index-3, data[it-3]); - //print("HERE[%] = %\n", it_index-2, data[it-2]); - //print("HERE[%] = %\n", it_index-1, data[it-1]); - //print("HERE[%] = %\n", it_index, it); - //print("HERE[%] = %\n", it_index+1, data[it+1]); - //print("HERE[%] = %\n", it_index+2, data[it+2]); - //print("HERE[%] = %\n", it_index+3, data[it+3]); - //print("HERE[%] = %\n", it_index+4, data[it+4]); - //} - - //print("%\n", < IN LOOP <\n"); - peek :string; - peek.data = mfi.map_info.data.data; - peek.count = 1; - seek := 0; - while true { - //sleep_milliseconds(10); - print("peeking '%'\n", peek); - peek.data += 100000; - seek += 100000; - if seek >= (186880600/2) - break; - } - print("-- peek complete\n"); - print("MFI.data.count is %\n", mfi.data.count); - sleep_milliseconds(10000); - print("-- unloading..."); - map_entire_file_end(*mfi); - print("done\n"); - sleep_milliseconds(10000); - //print("-- reading entire file"); - //read_entire_file(ar_file_path); - //print("done\n"); - //sleep_milliseconds(10000); - return; - } - - - db : Database; { - import_from_csv(*db, ar_file_path); - - for db.tasks print_task(it); + +// for db.tasks print_task(it); print_task :: (task: Task) { for task.times print("% |", it); print(" %\n", cast(string)task.name); } + return; // Stores data from database into binary file. -- cgit v1.2.3 From a6726bbe9a0217a25dd3007c2b28d1a3ceb30672 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 16 Mar 2023 23:07:00 +0000 Subject: Learning how to work with strings. --- ttt.jai | 57 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 14 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 392b692..e24419d 100644 --- a/ttt.jai +++ b/ttt.jai @@ -173,20 +173,23 @@ is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool return to_compare == test_a || to_compare == test_b; } -/* // Given an UTF8 encoded string, truncate it to length bytes without breaking any UTF8 character. // The string should have capacity for at least length + 1. // The terminating null byte ('\0') is not included in length. // Returns the truncated string length. -size_t truncate_string_utf8(char *string, size_t length) { - assert(string != NULL); +truncate_string_utf8 :: (str: string, length: s64) -> length: s64 { + assert(str.data != null); + assert(str.count >= length); + + data := str.data; + count := str.count; // Find index of first continuation byte. - size_t idx = length; - while (idx > 0 && ((string[idx - 1] & 0xC0) == 0x80)) { - idx--; + idx := length; + while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { + idx -= 1; } - size_t continuation_bytes = length - idx; + continuation_bytes := length - idx; // If string starts with continuation bytes, it's an invalid UTF8 string. if (idx == 0 && continuation_bytes > 0) { @@ -195,18 +198,19 @@ size_t truncate_string_utf8(char *string, size_t length) { // 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 && (string[idx - 1] & 0x80) == 0x00) - && !(continuation_bytes == 1 && (string[idx - 1] & 0xE0) == 0xC0) - && !(continuation_bytes == 2 && (string[idx - 1] & 0xF0) == 0xE0) - && !(continuation_bytes == 3 && (string[idx - 1] & 0xF8) == 0xF0) + && !(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 '+ 1' start byte. } - - string[length] = '\0'; + + ptr := data + length; + memset(ptr, 0, str.count - length); return length; } - +/* // Returns true when the string is empty or consists of white space characters. bool is_empty_string(const char *string) { for (int idx = 0; string[idx] != '\0'; idx++) { @@ -1274,6 +1278,31 @@ bool read_enter_confirmation(int row, int style, const char *message) { */ main :: () { + + print("---\n"); + + format :: (value: string) { + print("value is '%'\n", value); + } + + format("direct"); + va := "var A"; + format(va); + + sb := "var B"; + vb: [2048] u8; + memcpy(vb.data, sb.data, sb.count); + xb: string = xx sb; + print("> xb.count = %\n> xb.data = %\n", xb.count, xb.data); + format(xx vb); + + print("--- --- ---\n"); + xpto: string = "ç€dam"; + truncate_string_utf8(xpto, 3); + print(">'%'\n", xpto); + + return; + defer free_memory(); -- cgit v1.2.3 From efb6c8606d7e39a6ac9776bb1e4e28340aba2e29 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 16 Mar 2023 23:55:54 +0000 Subject: Fixed truncate_string_utf8. --- .fossil-settings/binary-glob | 15 --- .fossil-settings/crlf-glob | 1 - .fossil-settings/ignore-glob | 261 ------------------------------------------- ttt.jai | 12 +- 4 files changed, 8 insertions(+), 281 deletions(-) delete mode 100644 .fossil-settings/binary-glob delete mode 100644 .fossil-settings/crlf-glob delete mode 100644 .fossil-settings/ignore-glob (limited to 'ttt.jai') diff --git a/.fossil-settings/binary-glob b/.fossil-settings/binary-glob deleted file mode 100644 index 21f2881..0000000 --- a/.fossil-settings/binary-glob +++ /dev/null @@ -1,15 +0,0 @@ -*.ico -*.keystore -*.mp3 -*.mp4 -*.mkv -*.otf -*.png -*.so -*.tar.gz -*.tar.zst -*.tgz -*.ttf -*.wav -*.xcf -*.zip diff --git a/.fossil-settings/crlf-glob b/.fossil-settings/crlf-glob deleted file mode 100644 index 92bc64d..0000000 --- a/.fossil-settings/crlf-glob +++ /dev/null @@ -1 +0,0 @@ -CR+LF diff --git a/.fossil-settings/ignore-glob b/.fossil-settings/ignore-glob deleted file mode 100644 index 3c4efe2..0000000 --- a/.fossil-settings/ignore-glob +++ /dev/null @@ -1,261 +0,0 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ - -# Visual Studio 2015 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -project.fragment.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings -# but database connection strings (with potential passwords) will be unencrypted -#*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config -# NuGet v3's project.json files produces more ignoreable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions - -# Paket dependency manager -.paket/paket.exe -paket-files/ - -# FAKE - F# Make -.fake/ - -# JetBrains Rider -.idea/ -*.sln.iml - -# CodeRush -.cr/ - -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc \ No newline at end of file diff --git a/ttt.jai b/ttt.jai index e24419d..8a75568 100644 --- a/ttt.jai +++ b/ttt.jai @@ -183,6 +183,8 @@ truncate_string_utf8 :: (str: string, length: s64) -> length: s64 { data := str.data; count := str.count; + + // WIP simplify this if/else and try to use only idx or length // Find index of first continuation byte. idx := length; @@ -206,8 +208,7 @@ truncate_string_utf8 :: (str: string, length: s64) -> length: s64 { length -= (continuation_bytes + 1); // Remove '+ 1' start byte. } - ptr := data + length; - memset(ptr, 0, str.count - length); + memset(data + length, 0, count - length); return length; } /* @@ -1297,9 +1298,12 @@ main :: () { format(xx vb); print("--- --- ---\n"); - xpto: string = "ç€dam"; - truncate_string_utf8(xpto, 3); + xpto: string = copy_string("ç€dam"); + memcpy(vb.data, xpto.data, xpto.count); + truncate_string_utf8(xx vb, 5); + // memset(xpto.data, 0, 5); print(">'%'\n", xpto); + print(">'%'\n", cast(string)vb); return; -- cgit v1.2.3 From 7bd617825b0edca20bd4ef5ad4e49c4fd2ffde76 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 17 Mar 2023 10:23:58 +0000 Subject: Improve truncate_string to be aware of ASCII and UTF8 encodings. --- ttt.jai | 53 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 23 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 8a75568..d1e52ab 100644 --- a/ttt.jai +++ b/ttt.jai @@ -177,35 +177,42 @@ is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool // The string should have capacity for at least length + 1. // The terminating null byte ('\0') is not included in length. // Returns the truncated string length. -truncate_string_utf8 :: (str: string, length: s64) -> length: s64 { + +Text_Encoding :: enum u8 #specified { + ASCII :: 1; + UTF8 :: 2; +} + +// WIP TODO Ues compiler time code to see the auto bake being used... just for fun, once! :D +truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 #no_abc { // TODO Should I use #no_abc ? assert(str.data != null); assert(str.count >= length); data := str.data; count := str.count; - // WIP simplify this if/else and try to use only idx or length - - // Find index of first continuation byte. - idx := length; - while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { - idx -= 1; - } - continuation_bytes := length - idx; + #if encoding == .UTF8 { // WIP simplify this if/else and try to use only idx or length + // Find index of first continuation byte. + idx := length; + while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { + 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 '+ 1' start byte. + // 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 '+ 1' start byte. + } } memset(data + length, 0, count - length); @@ -1300,7 +1307,7 @@ main :: () { print("--- --- ---\n"); xpto: string = copy_string("ç€dam"); memcpy(vb.data, xpto.data, xpto.count); - truncate_string_utf8(xx vb, 5); + truncate_string(xx vb, 4); // memset(xpto.data, 0, 5); print(">'%'\n", xpto); print(">'%'\n", cast(string)vb); -- cgit v1.2.3 From b9971dce185f876b9358d399f23ebb4a1630ef1b Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 21 Mar 2023 00:18:46 +0000 Subject: Prototyping import_from_csv. --- ttt.jai | 44 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 33 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index d1e52ab..08f471a 100644 --- a/ttt.jai +++ b/ttt.jai @@ -187,11 +187,11 @@ Text_Encoding :: enum u8 #specified { truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 #no_abc { // TODO Should I use #no_abc ? assert(str.data != null); assert(str.count >= length); - + data := str.data; count := str.count; - #if encoding == .UTF8 { // WIP simplify this if/else and try to use only idx or length + #if encoding == .UTF8 { // Find index of first continuation byte. idx := length; while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { @@ -239,7 +239,8 @@ bool is_empty_string(const char *string) { // Uses strchr to replace all instances of find by replace. // Returns string. -char *replace_char(char *string, char find, char replace) { +char *replace_char(char *string, char find, char replace) { // TODO Use modules/String/module.jai:replace_chars + char *idx = string; while((idx = strchr(idx, find)) != NULL) { *idx = replace; @@ -809,7 +810,12 @@ import_from_csv :: (db: *Database, path: string) -> bool { task: Task; values := split(line, ","); // TODO Temporary memory... if file is too big this may break. - memcpy(task.name.data, values[0].data, xx min(task.name.count, values[0].count)); + + // Import task name. + name_length := min(task.name.count, values[0].count); + memcpy(task.name.data, values[0].data, name_length); + truncate_string(xx task.name, name_length); + advance(*values); for values { task.times[it_index] = string_to_int(it); @@ -1286,34 +1292,6 @@ bool read_enter_confirmation(int row, int style, const char *message) { */ main :: () { - - print("---\n"); - - format :: (value: string) { - print("value is '%'\n", value); - } - - format("direct"); - va := "var A"; - format(va); - - sb := "var B"; - vb: [2048] u8; - memcpy(vb.data, sb.data, sb.count); - xb: string = xx sb; - print("> xb.count = %\n> xb.data = %\n", xb.count, xb.data); - format(xx vb); - - print("--- --- ---\n"); - xpto: string = copy_string("ç€dam"); - memcpy(vb.data, xpto.data, xpto.count); - truncate_string(xx vb, 4); - // memset(xpto.data, 0, 5); - print(">'%'\n", xpto); - print(">'%'\n", cast(string)vb); - - return; - defer free_memory(); @@ -1339,7 +1317,7 @@ main :: () { { import_from_csv(*db, ar_file_path); -// for db.tasks print_task(it); + for db.tasks print_task(it); print_task :: (task: Task) { for task.times print("% |", it); -- cgit v1.2.3 From cd79b5a7e99178d1d51b864bf03d4da611e3e448 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 23 Mar 2023 23:56:53 +0000 Subject: Converting is_empty_string. --- ttt.jai | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 08f471a..e15b00f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -218,29 +218,32 @@ truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) memset(data + length, 0, count - length); return length; } -/* + // Returns true when the string is empty or consists of white space characters. -bool is_empty_string(const char *string) { - for (int idx = 0; string[idx] != '\0'; idx++) { - switch(string[idx]) { - case ' ': - case '\t': - case '\v': - case '\f': - case '\r': - case '\n': - break; - default: +is_empty_string :: (str: string) -> bool { + // TODO TEST + WIP + 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; } - +/* // Uses strchr to replace all instances of find by replace. // Returns string. -char *replace_char(char *string, char find, char replace) { // TODO Use modules/String/module.jai:replace_chars - +replace_char :: (str: string, char find, char replace) -> string { // TODO Use modules/String/module.jai:replace_chars + // TODO replace_chars :: (s: string, chars: string, replacement: u8) { char *idx = string; while((idx = strchr(idx, find)) != NULL) { *idx = replace; @@ -248,7 +251,7 @@ char *replace_char(char *string, char find, char replace) { // TODO Use modules/ } return string; } - +/* // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. int mvprintw_time(int y, int x, intmax_t time, int space) { @@ -1313,7 +1316,7 @@ main :: () { } - db : Database; + db: Database; { import_from_csv(*db, ar_file_path); -- cgit v1.2.3 From fdb58e513aeddb566c9de9f67d59cf5f25e8ead7 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 27 Mar 2023 17:42:16 +0100 Subject: Tested is_empty_string. --- ttt.jai | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index e15b00f..5848a35 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,7 +17,7 @@ // - release : jai ttt.jai -import_dir . -x64 -release // - debug : jai ttt.jai -import_dir . -x64 -#import "Basic"; +#import "Basic"()(MEMORY_DEBUGGER=true); #import "System"; #import "Math"; #import "POSIX"; @@ -219,10 +219,8 @@ truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) return length; } -// Returns true when the string is empty or consists of white space characters. +// Returns true when the string is empty or consists of space characters. is_empty_string :: (str: string) -> bool { - // TODO TEST - WIP for 0..str.count-1 { if str[it] == {; case #char "\0"; #through; @@ -251,7 +249,7 @@ replace_char :: (str: string, char find, char replace) -> string { // TODO Use m } return string; } -/* + // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. int mvprintw_time(int y, int x, intmax_t time, int space) { @@ -1296,6 +1294,8 @@ bool read_enter_confirmation(int row, int style, const char *message) { main :: () { + defer report_memory_leaks(); // TODO DEBUG + defer free_memory(); { // Initialize app directory. -- cgit v1.2.3 From e4bae939aea065fa5745e224fc0eb12d5136d1c4 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 27 Mar 2023 17:54:24 +0100 Subject: Fixing memory leaks. --- ttt.jai | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 5848a35..2dac60b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1213,15 +1213,16 @@ void *mem_alloc(size_t mem_size, const char *error_tag) { */ free_memory :: () { print(">> FREE <<\n"); - /* TODO - reset_database(&database); - reset_database(&archive); - free(string_buffer); string_buffer = NULL; - free(app_directory); app_directory = NULL; - free(db_file_path); db_file_path = NULL; - free(ar_file_path); ar_file_path = NULL; - */ + // TODO + //reset_database(&database); + //reset_database(&archive); + + //free(string_buffer); string_buffer = NULL; + free(app_directory); + free(db_file_path); + free(ar_file_path); + //reset_temporary_storage(); } /* @@ -1304,7 +1305,7 @@ main :: () { home_dir = "."; } - home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. + home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. TODO LEAK if success_path == false { print_error("Failed to find home directory '%'.", home_dir); return; -- cgit v1.2.3 From 5108f6062c1045b85ba0e624eab0833326604b86 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 28 Mar 2023 03:12:21 +0100 Subject: Prototyping database load and store. --- ttt.jai | 417 +++++++++++++++++++++++++++++----------------------------------- 1 file changed, 187 insertions(+), 230 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 2dac60b..42c68d7 100644 --- a/ttt.jai +++ b/ttt.jai @@ -14,14 +14,15 @@ // this program. If not, see . // Compilation commands: -// - release : jai ttt.jai -import_dir . -x64 -release -// - debug : jai ttt.jai -import_dir . -x64 +// - release : jai ttt.jai -import_dir . -quiet -x64 -release +// - debug : jai ttt.jai -import_dir . -quiet -x64 #import "Basic"()(MEMORY_DEBUGGER=true); #import "System"; #import "Math"; #import "POSIX"; #import "File"; +#import "File_Utilities"; #import "String"; #import "curses"; @@ -155,17 +156,6 @@ void show_processing() { refresh(); } -// Checks if file is exists and is accessible. -// Returns true when the file exists and is accessible. -bool is_file_accessible(const char *path) { - assert(path != NULL); - FILE *file = fopen(path, "r+"); - bool is_file_accessible = file != NULL; - if (is_file_accessible) { - fclose(file); - } - return is_file_accessible; -} */ // Returns true if string to_compare is equal to any of the other passed strings, false otherwise. @@ -237,19 +227,13 @@ is_empty_string :: (str: string) -> bool { } return true; } -/* -// Uses strchr to replace all instances of find by replace. -// Returns string. -replace_char :: (str: string, char find, char replace) -> string { // TODO Use modules/String/module.jai:replace_chars - // TODO replace_chars :: (s: string, chars: string, replacement: u8) { - char *idx = string; - while((idx = strchr(idx, find)) != NULL) { - *idx = replace; - idx++; - } - return string; -} +replace_char :: (str: string, find: u8, replace: u8) -> string { + assert(false, "Use modules/String/module.jai:replace_chars"); + // TODO Use modules/String/module.jai:replace_chars + return ""; +} +/* // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. int mvprintw_time(int y, int x, intmax_t time, int space) { @@ -606,52 +590,73 @@ void add_task_time(database_st *db, task_st *task, int day, int64_t time) { task->times[day] = add_int64(task->times[day], time); db->total_times[day] = add_int64(db->total_times[day], time); } +*/ // Resets database to the initial state and deallocates all memory taken by tasks. -void reset_database(database_st *db) { - assert(db != NULL); +reset_database :: (db: *Database) { + assert(db != null); - free(db->tasks); - memset(db, 0, SIZEOF_DATABASE_ST); - db->active_task = -1; - db->selected_task = -1; + free(db.tasks.data); + <tasks, SIZEOF_TASK_ST, db->count, file); +// +// fclose(file); +// return true; +// } + +// Stores data from database into binary file. +// Returns success. +store_database :: (db: *Database, path: string) -> success: bool { + assert(db != null, "Parameter 'db' is null."); + assert(xx path, "Parameter 'path' is empty."); // Open file. - FILE *file = fopen(path, "wb"); - if (file == NULL) { - print_error("Failed to open file '%s' while storing database: %s.", path, strerror(errno)); + file, open_success := file_open(path, for_writing = true); // log_errors: bool = true + if open_success == false { + print_error("Failed to open file '%' while storing database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } + defer file_close(*file); - fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); - fwrite(db, SIZEOF_DATABASE_ST, 1, file); - fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file); - - fclose(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. +/* bool load_database(database_st *db, const char *path) { assert(db != NULL); assert(path != NULL); - // Open file. + Open file. FILE *file = fopen(path, "rb"); if (file == NULL) { print_error("Failed to open file '%s' while loading database: %s.", path, strerror(errno)); return false; } - // Validate file signature. + Validate file signature. char file_signature[DB_FILE_SIGN_LENGTH]; fread(&file_signature, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); if (strncmp(file_signature, DB_FILE_SIGN, DB_FILE_SIGN_LENGTH) != 0) { @@ -660,10 +665,10 @@ bool load_database(database_st *db, const char *path) { return false; } - // Read database structure. + Read database structure. fread(db, SIZEOF_DATABASE_ST, 1, file); - // Reserve database capacity for tasks. + Reserve database capacity for tasks. size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; db->tasks = malloc(capacity_bytes); if (db->tasks == NULL && capacity_bytes > 0) { @@ -671,46 +676,110 @@ bool load_database(database_st *db, const char *path) { return false; } - // Read database tasks. + Read database tasks. fread(db->tasks, SIZEOF_TASK_ST, db->count, file); - // Make sure we are reading all the file. + Make sure we are reading all the file. assert(fgetc(file) == EOF); fclose(file); return true; -} +}*/ -// Exports data into CSV file. +// Loads data from binary file into database. // Returns success. -bool export_to_csv(const database_st *db, const char *path) { - assert(db != NULL); - assert(path != NULL); +load_database :: (db: *Database, path: string) -> success: bool { + assert(db != null, "Parameter 'db' is null."); + assert(xx path, "Parameter 'path' is empty."); - FILE *file = fopen(path, "w"); - if (file == NULL) { - print_error("Failed to open file '%s' while exporting to CSV: %s.", path, strerror(errno)); + // Open file. + file, open_success := file_open(path); // log_errors: bool = true + if open_success == false { + print_error("Failed to open file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } + defer file_close(*file); - fprintf(file, "%s,%s,%s,%s,%s,%s,%s,%s\n", - "task", "sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday" - ); + // 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 print_error("Failed to read file signature from '%'.", path); + if cast(string)file_signature != DB_FILE_SIGN_STR { + print_error("Invalid file signature."); + file_close(*file); + return false; + } - char name[TASK_NAME_BYTES]; - for (size_t idx = 0; idx < db->count; idx++) { - task_st *task = &db->tasks[idx]; - memcpy(name, task->name, TASK_NAME_BYTES); - replace_char(name, ',', ' '); - fprintf(file, "%s,%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 "\n", - name, task->times[0], task->times[1], task->times[2], task->times[3], task->times[4], task->times[5], task->times[6] - ); + // Read database structure. + read_success = file_read(file, db, size_of(Database)); + db.tasks.data = null; // Discard invalid pointer read into 'data'. + //if read_success == false print_error("Failed to read database info from '%'.", path); TODO + + //file_open :: (name: string, for_writing := false, keep_existing_content := false, log_errors := true) -> File, bool + //file_read :: (f: File, vdata: *void, bytes_to_read: s64) -> (success: bool, total_read: s64) + + + // Reserve database capacity for tasks. HACK + value := db.tasks.count; + d: [..] Task; + db.tasks = d; +// array_reserve(*db.tasks, value); +// db.tasks.count = value; + +// size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; +// db->tasks = malloc(capacity_bytes); +// if (db->tasks == NULL && capacity_bytes > 0) { +// print_error("Failed to allocate memory while loading database: %s.", strerror(errno)); +// return false; +// } +// + // Read database tasks. HACK + for 0..value-1 { + task: Task; + file_read(file, *task, size_of(Task)); + array_add(*db.tasks, task); } +// file_read(file, db.tasks.data, size_of(Task)*db.tasks.count); + + // Make sure we are reading all the file. + buffer: [1] u8; + success, bytes := file_read(file, *buffer, 1); + assert(bytes == 0); + + print("done importing CSV with a bunch of hacks\n"); - fclose(file); return true; } -*/ + +// Exports data into CSV file. +// Returns success. +export_to_csv :: (db: *Database, path: string) -> success: bool { + assert(db != null, "Parameter 'db' is null."); + assert(xx path, "Parameter 'path' is empty."); + // TODO Make sure (IN ALL PROCEDURES) we're not receiving an empty path. + + + 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 = ",")); + + buffer: [Task.name.count] u8; + name: string = xx buffer; + for db.tasks { + 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]); + } + + write_entire_file(path, *builder); + + return true; +} + // Imports CSV file into database. // Returns success. import_from_csv :: (db: *Database, path: string) -> bool { @@ -831,7 +900,7 @@ import_from_csv :: (db: *Database, path: string) -> bool { print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); reset_temporary_storage(); - return false; + return true; } /* // Appends task to the end of the CSV file. @@ -1295,6 +1364,9 @@ bool read_enter_confirmation(int row, int style, const char *message) { main :: () { + database: Database; + archive: Database; + defer report_memory_leaks(); // TODO DEBUG defer free_memory(); @@ -1308,158 +1380,43 @@ main :: () { home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. TODO LEAK if success_path == false { print_error("Failed to find home directory '%'.", home_dir); - return; + 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); - } - - - db: Database; - { - import_from_csv(*db, ar_file_path); - - for db.tasks print_task(it); - - print_task :: (task: Task) { - for task.times print("% |", it); - print(" %\n", cast(string)task.name); - } - return; - - // Stores data from database into binary file. - // Returns success. - store_database :: (db: Database, path: string) -> success: bool { - //assert(db != null, "Parameter 'db' is null."); - assert(xx path, "Parameter 'path' is empty."); - - // Open file. - file, open_success := file_open(path, for_writing = true); // log_errors: bool = true - if open_success == false { - print_error("Failed to open file '%' while storing database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! - return false; - } - defer file_close(*file); - - file_write(*file, DB_FILE_SIGN_STR); - file_write(*file, *db, size_of(Database)); - //fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); - //fwrite(db, SIZEOF_DATABASE_ST, 1, file); - //fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file); + make_directory_if_it_does_not_exist(app_directory, recursive = true); + } - return true; - } - - // Loads data from binary file into database. - // Returns success. - load_database :: (db: *Database, path: string) -> success: bool { - assert(db != null, "Parameter 'db' is null."); - assert(xx path, "Parameter 'path' is empty."); - - // Open file. - file, open_success := file_open(path); // log_errors: bool = true - if open_success == false { - print_error("Failed to open file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! - 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 print_error("Failed to read file signature from '%'.", path); - if cast(string)file_signature != DB_FILE_SIGN_STR { - print_error("Invalid file signature."); - file_close(*file); - return false; - } - - // Read database structure. - - read_success = file_read(file, db, size_of(Database)); - //if read_success == false print_error("Failed to read database info from '%'.", path); - - - //file_open :: (name: string, for_writing := false, keep_existing_content := false, log_errors := true) -> File, bool - //file_read :: (f: File, vdata: *void, bytes_to_read: s64) -> (success: bool, total_read: s64) - - return true; - } - - - // Loads data from binary file into database. - // Returns success. - /* - load_database :: ( + { // Initialize database and archive files if needed. - - bool load_database(database_st *db, const char *path) { - assert(db != NULL); - assert(path != NULL); - - // Open file. - FILE *file = fopen(path, "rb"); - if (file == NULL) { - print_error("Failed to open file '%s' while loading database: %s.", path, strerror(errno)); - return false; - } - - // Validate file signature. - char file_signature[DB_FILE_SIGN_LENGTH]; - fread(&file_signature, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); - if (strncmp(file_signature, DB_FILE_SIGN, DB_FILE_SIGN_LENGTH) != 0) { - print_error("Invalid file signature."); - fclose(file); - return false; + if (file_exists(db_file_path) == false) { + if (store_database(*database, db_file_path) == false) { + print_error("Failed to initialize database."); + exit(1); } - - // Read database structure. - fread(db, SIZEOF_DATABASE_ST, 1, file); - - // Reserve database capacity for tasks. - size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; - db->tasks = malloc(capacity_bytes); - if (db->tasks == NULL && capacity_bytes > 0) { - print_error("Failed to allocate memory while loading database: %s.", strerror(errno)); - return false; - } - - // Read database tasks. - fread(db->tasks, SIZEOF_TASK_ST, db->count, file); - - // Make sure we are reading all the file. - assert(fgetc(file) == EOF); - - fclose(file); - return true; } - */ - } - - return; // TODO DEBUG - - /* - db = &database; - reset_database(&database); - reset_database(&archive); - if (is_file_accessible(db_file_path) == false) { - if (store_database(&database, db_file_path) == false) { - print_error("Failed to initialize database."); - return; + if (file_exists(ar_file_path) == false) { + if (export_to_csv(*archive, ar_file_path) == false) { + print_error("Failed to initialize archive."); + exit(1); + } } - } - if (is_file_accessible(ar_file_path) == false) { - if (export_to_csv(&archive, ar_file_path) == false) { - print_error("Failed to initialize archive."); - return; - } + +// import_from_csv(*db, ar_file_path); +// +// for db.tasks print_task(it); +// +// print_task :: (task: Task) { +// for task.times print("% |", it); +// print(" %\n", cast(string)task.name); +// } +// return; } - */ args := get_command_line_arguments(); defer array_reset(*args); @@ -1531,19 +1488,19 @@ main :: () { return; } // TODO Implement -// if (load_database(&database, db_file_path) == false) { -// print_error("Failed to load database."); -// return; -// } -// if (import_from_csv(&database, args[it]) == false) { -// print_error("Failed to import CSV file."); -// return; -// } -// if (store_database(&database, db_file_path) == false) { -// print_error("Failed to store database."); -// return; -// } -// reset_database(&database); + if (load_database(*database, db_file_path) == false) { + print_error("Failed to load database."); + return; + } + if (import_from_csv(*database, args[it]) == false) { + print_error("Failed to import CSV file."); + return; + } + if (store_database(*database, db_file_path) == false) { + print_error("Failed to store database."); + return; + } + reset_database(*database); is_exit_requested = true; continue; } @@ -1555,15 +1512,15 @@ main :: () { return; } // TODO Implement -// if (load_database(&database, db_file_path) == false) { -// print_error("Failed to load database."); -// return; -// } -// if (export_to_csv(&database, args[it]) == false) { -// print_error("Failed to export CSV file."); -// return; -// } -// reset_database(&database); + if (load_database(*database, db_file_path) == false) { + print_error("Failed to load database."); + return; + } + if (export_to_csv(*database, args[it]) == false) { + print_error("Failed to export CSV file."); + return; + } + reset_database(*database); is_exit_requested = true; continue; } -- cgit v1.2.3 From bf8fb0e2a8bd35bfec3f3348d58367b9b6b54a54 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 28 Mar 2023 03:41:00 +0100 Subject: Playing with different hacks to store/load tasks. --- ttt.jai | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 42c68d7..9e6af5f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -711,36 +711,17 @@ load_database :: (db: *Database, path: string) -> success: bool { } // Read database structure. - read_success = file_read(file, db, size_of(Database)); - db.tasks.data = null; // Discard invalid pointer read into 'data'. + read_success = file_read(file, db, size_of(Database) - size_of([..] Task)); // HACK //if read_success == false print_error("Failed to read database info from '%'.", path); TODO - //file_open :: (name: string, for_writing := false, keep_existing_content := false, log_errors := true) -> File, bool - //file_read :: (f: File, vdata: *void, bytes_to_read: s64) -> (success: bool, total_read: s64) - - // Reserve database capacity for tasks. HACK - value := db.tasks.count; - d: [..] Task; - db.tasks = d; -// array_reserve(*db.tasks, value); -// db.tasks.count = value; - -// size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; -// db->tasks = malloc(capacity_bytes); -// if (db->tasks == NULL && capacity_bytes > 0) { -// print_error("Failed to allocate memory while loading database: %s.", strerror(errno)); -// return false; -// } -// - // Read database tasks. HACK - for 0..value-1 { - task: Task; - file_read(file, *task, size_of(Task)); - array_add(*db.tasks, task); - } -// file_read(file, db.tasks.data, size_of(Task)*db.tasks.count); - + tasks_count: s64; + file_read(file, *tasks_count, size_of(s64)); + t: [2048]u8; file_read(file, *t, size_of([..] Task) - size_of(s64)); // HACK + array_reserve(*db.tasks, tasks_count); + 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: [1] u8; success, bytes := file_read(file, *buffer, 1); -- cgit v1.2.3 From 2e42845bda874cddfb1e0feed8448cad91dd0a24 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 29 Mar 2023 23:48:23 +0100 Subject: Changed the way the tasks array is implemented on Database. --- ttt.jai | 63 +++++++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 16 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 9e6af5f..1d7edfb 100644 --- a/ttt.jai +++ b/ttt.jai @@ -57,7 +57,8 @@ Database :: struct { active_idx : s64; selected_idx : s64; total_times : [NUM_WEEK_DAYS] s64; - tasks : [..] Task; + tasks : [] Task; + capacity : s64; } // const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; @@ -316,14 +317,49 @@ task_st *get_selected_task(database_st *db) { return task; } */ -// Creates new task stored at location given by task pointer. +// Adds a task to the database. // If necessary, expands database capacity. // Returns success. -add_task :: (db: *Database, task: *Task) -> success: bool { - idx := db.tasks.count; - maybe_grow(*db.tasks); // TODO Check for errors? +add_task :: (db: *Database, task: Task) -> success: bool { + assert(db != null, "Parameter 'db' is null."); + +// idx := db.tasks.count; +// maybe_grow(*db.tasks); // TODO Check for errors? +// db.tasks.count += 1; +// db.tasks[idx] = task; + + if (db.tasks.count == S64_MAX) { + print_error("Database reached maximum capacity."); + return false; + } + + // If necessary, expand database capacity. + current_capacity := db.capacity; + if ((db.tasks.count + 1) > current_capacity) { + new_capacity := + ifx current_capacity == 0 then 8 else + ifx current_capacity > (S64_MAX >> 1) then S64_MAX else + current_capacity << 1; + + print("expanding from % to %\n", current_capacity, new_capacity); + new_tasks := realloc(db.tasks.data, new_capacity * size_of(Task), current_capacity * size_of(Task)); + if (new_tasks == null) { + print_error("Failed to expand database."); + return false; + } + + db.tasks.data = new_tasks; + db.capacity = new_capacity; + } + db.tasks.count += 1; - db.tasks[idx] = task; + db.tasks[db.tasks.count-1] = task; + + // Adjust selected task. + if (db.selected_idx < 0) { + db.selected_idx = db.tasks.count-1; + } + return true; } /* @@ -637,7 +673,7 @@ store_database :: (db: *Database, path: string) -> success: bool { 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); + file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); return true; } @@ -706,21 +742,16 @@ load_database :: (db: *Database, path: string) -> success: bool { if read_success == false print_error("Failed to read file signature from '%'.", path); if cast(string)file_signature != DB_FILE_SIGN_STR { print_error("Invalid file signature."); - file_close(*file); return false; } // Read database structure. - read_success = file_read(file, db, size_of(Database) - size_of([..] Task)); // HACK + read_success = file_read(file, db, size_of(Database)); //if read_success == false print_error("Failed to read database info from '%'.", path); TODO - // Reserve database capacity for tasks. HACK - tasks_count: s64; - file_read(file, *tasks_count, size_of(s64)); - t: [2048]u8; file_read(file, *t, size_of([..] Task) - size_of(s64)); // HACK - array_reserve(*db.tasks, tasks_count); - file_read(file, db.tasks.data, size_of(Task)*tasks_count); - db.tasks.count = tasks_count; + // Reserve database capacity for tasks. + db.tasks.data = alloc(db.tasks.count * size_of(Task)); + db.capacity = db.tasks.count; // Make sure we are reading all the file. buffer: [1] u8; -- cgit v1.2.3 From a7d4d3038a11fd8b6f8a1439840c2034ebb3ae01 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 30 Mar 2023 00:36:31 +0100 Subject: Fixed bug loading database. --- ttt.jai | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 1d7edfb..4c79f86 100644 --- a/ttt.jai +++ b/ttt.jai @@ -753,9 +753,14 @@ load_database :: (db: *Database, path: string) -> success: bool { db.tasks.data = alloc(db.tasks.count * size_of(Task)); db.capacity = db.tasks.count; + // Read database tasks. + file_read(file, db.tasks.data, size_of(Task) * db.tasks.count); // TODO WIP WIP WIP Does this generate stable memory or temporary? + // Make sure we are reading all the file. + // TODO Clean this. WIP WIP WIP buffer: [1] u8; success, bytes := file_read(file, *buffer, 1); + print("'%'\n", buffer); assert(bytes == 0); print("done importing CSV with a bunch of hacks\n"); -- cgit v1.2.3 From f82ce565436dc04d58c85d55fb78038acf0191b2 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 31 Mar 2023 00:38:26 +0100 Subject: Fix load_database and free_memory. --- ttt.jai | 172 ++++++++++++++++++++-------------------------------------------- 1 file changed, 52 insertions(+), 120 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 4c79f86..daa826b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -70,9 +70,9 @@ Database :: struct { // // // -// database_st database = { .tasks = NULL }; -// database_st archive = { .tasks = NULL }; -// database_st *db = NULL; + +database : Database; +archive : Database; is_autosave_enabled := true; // int countdown_to_autosave = -1; app_directory : string; @@ -631,32 +631,10 @@ void add_task_time(database_st *db, task_st *task, int day, int64_t time) { // Resets database to the initial state and deallocates all memory taken by tasks. reset_database :: (db: *Database) { assert(db != null); - free(db.tasks.data); <tasks, SIZEOF_TASK_ST, db->count, file); -// -// fclose(file); -// return true; -// } - // Stores data from database into binary file. // Returns success. store_database :: (db: *Database, path: string) -> success: bool { @@ -678,50 +656,6 @@ store_database :: (db: *Database, path: string) -> success: bool { return true; } -// Loads data from binary file into database. -// Returns success. -/* -bool load_database(database_st *db, const char *path) { - assert(db != NULL); - assert(path != NULL); - - Open file. - FILE *file = fopen(path, "rb"); - if (file == NULL) { - print_error("Failed to open file '%s' while loading database: %s.", path, strerror(errno)); - return false; - } - - Validate file signature. - char file_signature[DB_FILE_SIGN_LENGTH]; - fread(&file_signature, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file); - if (strncmp(file_signature, DB_FILE_SIGN, DB_FILE_SIGN_LENGTH) != 0) { - print_error("Invalid file signature."); - fclose(file); - return false; - } - - Read database structure. - fread(db, SIZEOF_DATABASE_ST, 1, file); - - Reserve database capacity for tasks. - size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST; - db->tasks = malloc(capacity_bytes); - if (db->tasks == NULL && capacity_bytes > 0) { - print_error("Failed to allocate memory while loading database: %s.", strerror(errno)); - return false; - } - - Read database tasks. - fread(db->tasks, SIZEOF_TASK_ST, db->count, file); - - Make sure we are reading all the file. - assert(fgetc(file) == EOF); - - fclose(file); - return true; -}*/ - // Loads data from binary file into database. // Returns success. load_database :: (db: *Database, path: string) -> success: bool { @@ -747,24 +681,25 @@ load_database :: (db: *Database, path: string) -> success: bool { // Read database structure. read_success = file_read(file, db, size_of(Database)); - //if read_success == false print_error("Failed to read database info from '%'.", path); TODO + // TODO Use print_error or assert? + if read_success == false { + print_error("Failed to read database info from '%'.", path); + return false; + } + assert(read_success == true, "Failed to read database info from '%'.", path); // Reserve database capacity for tasks. db.tasks.data = alloc(db.tasks.count * size_of(Task)); db.capacity = db.tasks.count; // Read database tasks. - file_read(file, db.tasks.data, size_of(Task) * db.tasks.count); // TODO WIP WIP WIP Does this generate stable memory or temporary? + file_read(file, db.tasks.data, size_of(Task) * db.tasks.count); // Make sure we are reading all the file. - // TODO Clean this. WIP WIP WIP - buffer: [1] u8; + buffer: u8; success, bytes := file_read(file, *buffer, 1); - print("'%'\n", buffer); - assert(bytes == 0); + assert(bytes == 0, "Unexpected content found at the end of file '%'.", path); - print("done importing CSV with a bunch of hacks\n"); - return true; } @@ -1298,11 +1233,8 @@ void *mem_alloc(size_t mem_size, const char *error_tag) { */ free_memory :: () { - print(">> FREE <<\n"); - - // TODO - //reset_database(&database); - //reset_database(&archive); + reset_database(*database); + reset_database(*archive); //free(string_buffer); string_buffer = NULL; free(app_directory); @@ -1378,12 +1310,9 @@ bool read_enter_confirmation(int row, int style, const char *message) { return getch() == '\n'; } */ - + main :: () { - database: Database; - archive: Database; - defer report_memory_leaks(); // TODO DEBUG defer free_memory(); @@ -1422,17 +1351,6 @@ main :: () { exit(1); } } - - -// import_from_csv(*db, ar_file_path); -// -// for db.tasks print_task(it); -// -// print_task :: (task: Task) { -// for task.times print("% |", it); -// print(" %\n", cast(string)task.name); -// } -// return; } args := get_command_line_arguments(); @@ -1490,32 +1408,31 @@ main :: () { "- During intensive tasks such as saving to file or recalculating totals times,\n", " a diamond symbol is shown on the top left corner.\n" ); - return; + 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); - return; + exit(0); } if is_equal_to_any(args[it], "--import-csv", "-i") { it += 1; if it >= args.count { print_error("Missing CSV file path to import."); - return; + exit(1); } - // TODO Implement if (load_database(*database, db_file_path) == false) { print_error("Failed to load database."); - return; + exit(1); } if (import_from_csv(*database, args[it]) == false) { print_error("Failed to import CSV file."); - return; + exit(1); } if (store_database(*database, db_file_path) == false) { print_error("Failed to store database."); - return; + exit(1); } reset_database(*database); is_exit_requested = true; @@ -1526,16 +1443,15 @@ main :: () { it += 1; if it >= args.count { print_error("Missing CSV file path to export."); - return; + exit(1); } - // TODO Implement if (load_database(*database, db_file_path) == false) { print_error("Failed to load database."); - return; + exit(1); } if (export_to_csv(*database, args[it]) == false) { print_error("Failed to export CSV file."); - return; + exit(1); } reset_database(*database); is_exit_requested = true; @@ -1548,26 +1464,27 @@ main :: () { } print_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]); - return; + exit(1); } if is_exit_requested { - return; + exit(0); } } - /* - if (load_database(&database, db_file_path) == false) { + + if (load_database(*database, db_file_path) == false) { print_error("Failed to load database."); - return; + exit(1); } - initialize_tui(); - - signal(SIGTERM, exit_gracefully); - signal(SIGINT, exit_gracefully); - signal(SIGQUIT, exit_gracefully); - signal(SIGHUP, exit_gracefully); + //initialize_tui(); + /* + // TODO Remove this?! + //signal(SIGTERM, exit_gracefully); + //signal(SIGINT, exit_gracefully); + //signal(SIGQUIT, exit_gracefully); + //signal(SIGHUP, exit_gracefully); flushinp(); ungetch(KEY_RESIZE); @@ -2014,7 +1931,7 @@ main :: () { } */ - +// TODO DEBUG print_owner_allocator :: (tag: string, memory: *void) { owner := "unkown"; @@ -2023,3 +1940,18 @@ print_owner_allocator :: (tag: string, memory: *void) { print("'%' belongs to '%'\n", tag, owner); } + +// TODO DEBUG +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] + ); + } +} -- cgit v1.2.3 From ee5fa34d14288a4ce79d77a0d3a89cc14b16a6b1 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 31 Mar 2023 01:52:05 +0100 Subject: Implemented initialize_tui. --- curses.jai | 36 ++++++++--- ttt.jai | 216 ++++++++++++++++++++++++++++++------------------------------- 2 files changed, 132 insertions(+), 120 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index bec9db5..866d99e 100644 --- a/curses.jai +++ b/curses.jai @@ -78,12 +78,30 @@ tinfo :: #system_library "libtinfo"; // Required by some of ncurses functions. // I suspect this is an attempt to help the compilers to include libtinfo automatically. // ncurses :: #system_library "libncurses"; ncurses :: #library "libncurses"; -initscr :: () -> *WINDOW #foreign ncurses; -getch :: () -> s8 #foreign ncurses; -endwin :: () -> void #foreign ncurses; - -curs_set :: (visibility: s32) -> s32 #foreign ncurses; -mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; -noecho :: () -> s32 #foreign ncurses; -box :: (win :*WINDOW, verch :u8, horch :u8) -> s32 #foreign ncurses; - + +COLOR_BLACK :: 0; +COLOR_RED :: 1; +COLOR_GREEN :: 2; +COLOR_YELLOW :: 3; +COLOR_BLUE :: 4; +COLOR_MAGENTA :: 5; +COLOR_CYAN :: 6; +COLOR_WHITE :: 7; + +stdscr : *WINDOW; + +initscr :: () -> *WINDOW #foreign ncurses; +getch :: () -> s8 #foreign ncurses; +endwin :: () -> void #foreign ncurses; +cbreak :: () -> void #foreign ncurses; +start_color :: () -> void #foreign ncurses; +use_default_colors :: () -> void #foreign ncurses; + + +keypad :: (win: *WINDOW, bf: bool) -> s32 #foreign ncurses; + +curs_set :: (visibility: s32) -> s32 #foreign ncurses; +mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; +noecho :: () -> s32 #foreign ncurses; +box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; +init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; diff --git a/ttt.jai b/ttt.jai index daa826b..96a5fdb 100644 --- a/ttt.jai +++ b/ttt.jai @@ -82,13 +82,18 @@ ar_file_path : string; // size_t string_buffer_size = 0; // int size_x, size_y, pos_x, pos_y; -// typedef enum { -// STYLE_SELECTED = 1, -// STYLE_SELECTED_INVERTED, -// STYLE_ACTIVE, -// STYLE_ACTIVE_SELECTED, -// STYLE_ERROR, -// } styles_et; +Styles :: enum s16 { + STYLE_SELECTED :: 1; + STYLE_SELECTED_INVERTED; + STYLE_ACTIVE; + STYLE_ACTIVE_SELECTED; + STYLE_ERROR; +} + +Layouts :: enum u8 { + NORMAL; + COMPACT; +} error_window : *WINDOW = null; error_time_limit := Apollo_Time.{0, 0}; @@ -213,7 +218,7 @@ truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) // Returns true when the string is empty or consists of space characters. is_empty_string :: (str: string) -> bool { for 0..str.count-1 { - if str[it] == {; + if str[it] == { case #char "\0"; #through; case #char "\t"; #through; // horizontal tab case #char "\n"; #through; // line feed @@ -926,114 +931,105 @@ bool is_database_full(database_st *db) { assert(db != NULL); return db->count >= MAX_DATABASE_TASKS; } +*/ -#define INPUT_TIMEOUT_MS 1000 -#define INPUT_AWAIT_INF -1 - -#define NUM_HEADER_ROWS 1 -#define NUM_FOOTER_ROWS 1 -#define NUM_COLUMNS 9 +INPUT_TIMEOUT_MS :: 1000; +INPUT_AWAIT_INF :: -1; -#define L_TITLE_IDX 0 -#define L_DAYS_IDX 1 -#define L_TOTAL_IDX 8 +NUM_HEADER_ROWS :: 1; +NUM_FOOTER_ROWS :: 1; +NUM_COLUMNS :: 9; -typedef enum { - L_NORMAL, - L_COMPACT, - NUM_LAYOUTS, -} layouts_et; +L_TITLE_IDX :: 0; +L_DAYS_IDX :: 1; +L_TOTAL_IDX :: 8; -typedef struct { - char *header; - int width; - int alignment_offset; - char alignment; -} column_st; +Column :: struct { + header : string; + width : int; + alignment_offset : int; + alignment : u8; +} -typedef struct { - column_st columns[NUM_COLUMNS]; - char *archive_title; -} layout_st; +Layout :: struct { + columns : [NUM_COLUMNS] Column; + archive_title : string; +} -layout_st layouts[NUM_LAYOUTS]; -int layout_tasks_rows; -bool is_terminal_too_small = true; +layouts : [#run type_info(Layouts).values.count] Layout; +layout_tasks_rows : int; +is_terminal_too_small := true; -void initialize_tui() { +initialize_tui :: () { // Normal layout. - layouts[L_NORMAL] = (layout_st) { - .archive_title = " Archive ", - .columns = { - { .header = " Task Time Tracker v" VERSION " ", .width = -1, .alignment = 'L' }, - { .header = " Sun ", .width = 7, .alignment = 'C' }, - { .header = " Mon ", .width = 7, .alignment = 'C' }, - { .header = " Tue ", .width = 7, .alignment = 'C' }, - { .header = " Wed ", .width = 7, .alignment = 'C' }, - { .header = " Thu ", .width = 7, .alignment = 'C' }, - { .header = " Fri ", .width = 7, .alignment = 'C' }, - { .header = " Sat ", .width = 7, .alignment = 'C' }, - { .header = " Total ", .width = 9, .alignment = 'C' }, - } + 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[L_COMPACT] = (layout_st) { - .archive_title = " Archive ", - .columns = { - { .header = " TTT " VERSION " ", .width = -1, .alignment = 'L' }, - { .header = " S ", .width = 5, .alignment = 'C' }, - { .header = " M ", .width = 5, .alignment = 'C' }, - { .header = " T ", .width = 5, .alignment = 'C' }, - { .header = " W ", .width = 5, .alignment = 'C' }, - { .header = " T ", .width = 5, .alignment = 'C' }, - { .header = " F ", .width = 5, .alignment = 'C' }, - { .header = " S ", .width = 5, .alignment = 'C' }, - { .header = " # ", .width = 5, .alignment = 'C' }, - } + 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_st *layout = layouts; layout < layouts + NUM_LAYOUTS; layout++) { - for (column_st *col = layout->columns; col < layout->columns + NUM_COLUMNS; col++) { - int offset; - switch(col->alignment) { - default: - case 'L': + for * layout: layouts { + for * col: layout.columns { + offset: int; + if col.alignment == { + case #char "L"; offset = 0; - break; - - case 'C': - offset = ((col->width - strlen(col->header)) / 2); - break; - - case 'R': - offset = (col->width - strlen(col->header)); - break; + case #char "C"; + offset = ((col.width - col.header.count) / 2); + case #char "R"; + offset = (col.width - col.header.count); } - col->alignment_offset = offset; + col.alignment_offset = offset; } } - setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. - initscr(); // Start curses mode. + // TODO + //setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. + stdscr = initscr(); // Start curses mode. cbreak(); // Line buffering disabled; pass on everty thing to me. - keypad(stdscr, TRUE); // I need that nifty F1. + keypad(stdscr, true); // I need those nifty F1..F12. curs_set(0); // Set cursor invisible. noecho(); // Disable echoing input characters. // Initialize pairs of colors. start_color(); use_default_colors(); // Using default (-1) instead of COLOR_BLACK. - init_pair(STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN); - init_pair(STYLE_SELECTED_INVERTED, COLOR_CYAN, -1); - init_pair(STYLE_ACTIVE, COLOR_BLUE, -1); - init_pair(STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); - init_pair(STYLE_ERROR, COLOR_RED, -1); + init_pair(xx Styles.STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN); + init_pair(xx Styles.STYLE_SELECTED_INVERTED, COLOR_CYAN, -1); + init_pair(xx Styles.STYLE_ACTIVE, COLOR_BLUE, -1); + init_pair(xx Styles.STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); + init_pair(xx Styles.STYLE_ERROR, COLOR_RED, -1); } +/* void update_layout() { // Calculate number of available rows to display tasks. layout_tasks_rows = (size_y - NUM_HEADER_ROWS - NUM_FOOTER_ROWS); @@ -1472,13 +1468,12 @@ main :: () { } } - if (load_database(*database, db_file_path) == false) { print_error("Failed to load database."); exit(1); } - //initialize_tui(); + initialize_tui(); /* // TODO Remove this?! //signal(SIGTERM, exit_gracefully); @@ -1868,32 +1863,31 @@ main :: () { timeout(INPUT_TIMEOUT_MS); } - + */ // Save any unsaved changes. - show_processing(); - bool error_saving = false; - if (db == &archive) { - if (export_to_csv(&archive, ar_file_path) == false) { - print_error("Failed to save archive."); - error_saving |= true; - } - } - if (countdown_to_autosave > 0 || is_autosave_enabled == false) { - if (store_database(&database, db_file_path) == false) { - print_error("Failed to save database."); - error_saving |= true; - } - } - if (error_saving) { - print_error("Press any key to close."); - draw_error_window(); - timeout(INPUT_AWAIT_INF); - getch(); - } +// show_processing(); + error_saving := false; +// if (db == &archive) { +// if (export_to_csv(&archive, ar_file_path) == false) { +// print_error("Failed to save archive."); +// error_saving |= true; +// } +// } +// if (countdown_to_autosave > 0 || is_autosave_enabled == false) { +// if (store_database(&database, db_file_path) == false) { +// print_error("Failed to save database."); +// error_saving |= true; +// } +// } +// if (error_saving) { +// print_error("Press any key to close."); +// draw_error_window(); +// timeout(INPUT_AWAIT_INF); +// getch(); +// } endwin(); - return error_saving ? EXIT_FAILURE : EXIT_SUCCESS; - */ + exit(xx ifx error_saving then 1 else 0); } -- cgit v1.2.3 From 98a21ce9edcd22bf7a9dde62f317f143e289ecf5 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 4 Apr 2023 17:45:05 +0100 Subject: WIP Making ncurses work a bit. --- curses.jai | 303 +++++++++++++++++++++++++++++++++++++++++++++++++------------ sizeof.c | 47 ++++++++++ ttt.jai | 213 +++++++++++++++++++++---------------------- 3 files changed, 397 insertions(+), 166 deletions(-) create mode 100644 sizeof.c (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index 866d99e..532054a 100644 --- a/curses.jai +++ b/curses.jai @@ -1,62 +1,62 @@ ldat :: struct { - text :*void; /* text of the line */ - firstchar :s16; /* first changed character in the line */ - lastchar :s16; /* last changed character in the line */ - oldindex :s16; /* index of the line at last update */ + text :*void; /* text of the line */ + firstchar :s16; /* first changed character in the line */ + lastchar :s16; /* last changed character in the line */ + oldindex :s16; /* index of the line at last update */ }; // TODO This also works... -WINDOW :: struct { +WINDOWx :: struct { data : [88] u8; } -WINDOWx :: struct { - _cury, _curx : s16; /* current cursor position */ - - /* window location and size */ - _maxy, _maxx : s16; /* maximums of x and y, NOT window size */ - _begy, _begx : s16; /* screen coords of upper-left-hand corner */ - - _flags :s16; /* window state flags */ - - /* attribute tracking */ - _attrs :u8; /* current attribute for non-space character */ - _bkgd :u8; /* current background char/attribute pair */ - - /* option values set by user */ - _notimeout :bool; /* no time out on function-key entry? */ - _clear :bool; /* consider all data in the window invalid? */ - _leaveok :bool; /* OK to not reset cursor on exit? */ - _scroll :bool; /* OK to scroll this window? */ - _idlok :bool; /* OK to use insert/delete line? */ - _idcok :bool; /* OK to use insert/delete char? */ - _immed :bool; /* window in immed mode? (not yet used) */ - _sync :bool; /* window in sync mode? */ - _use_keypad :bool; /* process function keys into KEY_ symbols? */ - _delay :s32; /* 0 = nodelay, <0 = blocking, >0 = delay */ - - _line :*ldat; /* the actual line data */ - - /* global screen state */ - _regtop :s16; /* top line of scrolling region */ - _regbottom :s16; /* bottom line of scrolling region */ - - /* these are used only if this is a sub-window */ - _parx :s32; /* x coordinate of this window in parent */ - _pary :s32; /* y coordinate of this window in parent */ +WINDOW :: struct { + _cury, _curx : s16; /* current cursor position */ + + /* window location and size */ + _maxy, _maxx : s16; /* maximums of x and y, NOT window size */ + _begy, _begx : s16; /* screen coords of upper-left-hand corner */ + + _flags :s16; /* window state flags */ + + /* attribute tracking */ + _attrs :u8; /* current attribute for non-space character */ + _bkgd :u8; /* current background char/attribute pair */ + + /* option values set by user */ + _notimeout :bool; /* no time out on function-key entry? */ + _clear :bool; /* consider all data in the window invalid? */ + _leaveok :bool; /* OK to not reset cursor on exit? */ + _scroll :bool; /* OK to scroll this window? */ + _idlok :bool; /* OK to use insert/delete line? */ + _idcok :bool; /* OK to use insert/delete char? */ + _immed :bool; /* window in immed mode? (not yet used) */ + _sync :bool; /* window in sync mode? */ + _use_keypad :bool; /* process function keys into KEY_ symbols? */ + _delay :s32; /* 0 = nodelay, <0 = blocking, >0 = delay */ + + _line :*ldat; /* the actual line data */ + + /* global screen state */ + _regtop :s16; /* top line of scrolling region */ + _regbottom :s16; /* bottom line of scrolling region */ + + /* these are used only if this is a sub-window */ + _parx :s32; /* x coordinate of this window in parent */ + _pary :s32; /* y coordinate of this window in parent */ _parent :*WINDOW; /* pointer to parent if a sub-window */ - /* these are used only if this is a pad */ - pdat :: struct - { - _pad_y, _pad_x :s16 ; - _pad_top, _pad_left :s16; - _pad_bottom, _pad_right :s16; - }; + /* these are used only if this is a pad */ + pdat :: struct + { + _pad_y, _pad_x :s16 ; + _pad_top, _pad_left :s16; + _pad_bottom, _pad_right :s16; + }; _pad :pdat; - - _yoffset :s16; /* real begy is _begy + _yoffset */ + + _yoffset :s16; /* real begy is _begy + _yoffset */ /* #if NCURSES_WIDECHAR @@ -79,29 +79,214 @@ tinfo :: #system_library "libtinfo"; // Required by some of ncurses functions. // ncurses :: #system_library "libncurses"; ncurses :: #library "libncurses"; -COLOR_BLACK :: 0; -COLOR_RED :: 1; -COLOR_GREEN :: 2; -COLOR_YELLOW :: 3; -COLOR_BLUE :: 4; -COLOR_MAGENTA :: 5; -COLOR_CYAN :: 6; -COLOR_WHITE :: 7; - stdscr : *WINDOW; initscr :: () -> *WINDOW #foreign ncurses; -getch :: () -> s8 #foreign ncurses; +getch :: () -> s32 #foreign ncurses; endwin :: () -> void #foreign ncurses; cbreak :: () -> void #foreign ncurses; start_color :: () -> void #foreign ncurses; use_default_colors :: () -> void #foreign ncurses; - +flushinp :: () -> s32 #foreign ncurses; keypad :: (win: *WINDOW, bf: bool) -> s32 #foreign ncurses; +ungetch :: (ch: s32) -> s32 #foreign ncurses; +attrset :: (attrs: s32) -> s32 #foreign ncurses; +erase :: () -> s32 #foreign ncurses; curs_set :: (visibility: s32) -> s32 #foreign ncurses; mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; noecho :: () -> s32 #foreign ncurses; box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; +timeout :: (delay: s32) -> void #foreign ncurses; +mvaddch :: (y: s32, x: s32, ch: u32) -> s32 #foreign ncurses; +clear :: () -> s32 #foreign ncurses; +refresh :: () -> s32 #foreign ncurses; + +getmaxyx :: inline (win: *WINDOW, y: *s32, x: *s32) { < s32 { return ifx win == null then ERR else win._maxx + 1; } +getmaxy :: inline (win: *WINDOW) -> s32 { return ifx win == null then ERR else win._maxy + 1; } +getcurx :: inline (win: *WINDOW) -> s32 { return ifx win == null then ERR else win._curx; } +getcury :: inline (win: *WINDOW) -> s32 { return ifx win == null then ERR else win._cury; } + +//#if CPU == .X64 { +//typedef unsigned chtype; // Used for mask and shift vars. +//typedef unsigned mmask_t; +//} else { +//typedef uint32_t chtype; +//typedef uint32_t mmask_t; +//} +NCURSES_ATTR_SHIFT :: 8; +NCURSES_BITS :: inline (mask: u32, shift: u32) -> u32 { return mask << (shift + NCURSES_ATTR_SHIFT); } + +A_NORMAL :: 0; +A_ATTRIBUTES :: #run NCURSES_BITS(~(cast(u32)(1 - 1)),0); +A_CHARTEXT :: #run (NCURSES_BITS(1,0) - 1); +A_COLOR :: #run NCURSES_BITS(((1) << 8) - 1,0); +A_STANDOUT :: #run NCURSES_BITS(1,8); +A_UNDERLINE :: #run NCURSES_BITS(1,9); +A_REVERSE :: #run NCURSES_BITS(1,10); +A_BLINK :: #run NCURSES_BITS(1,11); +A_DIM :: #run NCURSES_BITS(1,12); +A_BOLD :: #run NCURSES_BITS(1,13); +A_ALTCHARSET :: #run NCURSES_BITS(1,14); +A_INVIS :: #run NCURSES_BITS(1,15); +A_PROTECT :: #run NCURSES_BITS(1,16); +A_HORIZONTAL :: #run NCURSES_BITS(1,17); +A_LEFT :: #run NCURSES_BITS(1,18); +A_LOW :: #run NCURSES_BITS(1,19); +A_RIGHT :: #run NCURSES_BITS(1,20); +A_TOP :: #run NCURSES_BITS(1,21); +A_VERTICAL :: #run NCURSES_BITS(1,22); +A_ITALIC :: #run NCURSES_BITS(1,23); // ncurses extension + +// VT100 symbols begin here. +ACS_ULCORNER :: #run A_ALTCHARSET | #char "l"; /* upper left corner */ +ACS_LLCORNER :: #run A_ALTCHARSET | #char "m"; /* lower left corner */ +ACS_URCORNER :: #run A_ALTCHARSET | #char "k"; /* upper right corner */ +ACS_LRCORNER :: #run A_ALTCHARSET | #char "j"; /* lower right corner */ +ACS_LTEE :: #run A_ALTCHARSET | #char "t"; /* tee pointing right */ +ACS_RTEE :: #run A_ALTCHARSET | #char "u"; /* tee pointing left */ +ACS_BTEE :: #run A_ALTCHARSET | #char "v"; /* tee pointing up */ +ACS_TTEE :: #run A_ALTCHARSET | #char "w"; /* tee pointing down */ +ACS_HLINE :: #run A_ALTCHARSET | #char "q"; /* horizontal line */ +ACS_VLINE :: #run A_ALTCHARSET | #char "x"; /* vertical line */ +ACS_PLUS :: #run A_ALTCHARSET | #char "n"; /* large plus or crossover */ +ACS_S1 :: #run A_ALTCHARSET | #char "o"; /* scan line 1 */ +ACS_S9 :: #run A_ALTCHARSET | #char "s"; /* scan line 9 */ +ACS_DIAMOND :: #run A_ALTCHARSET | #char "`"; /* diamond */ +ACS_CKBOARD :: #run A_ALTCHARSET | #char "a"; /* checker board (stipple) */ +ACS_DEGREE :: #run A_ALTCHARSET | #char "f"; /* degree symbol */ +ACS_PLMINUS :: #run A_ALTCHARSET | #char "g"; /* plus/minus */ +ACS_BULLET :: #run A_ALTCHARSET | #char "~"; /* bullet */ +// Teletype 5410v1 symbols begin here. +ACS_LARROW :: #run A_ALTCHARSET | #char ","; /* arrow pointing left */ +ACS_RARROW :: #run A_ALTCHARSET | #char "+"; /* arrow pointing right */ +ACS_DARROW :: #run A_ALTCHARSET | #char "."; /* arrow pointing down */ +ACS_UARROW :: #run A_ALTCHARSET | #char "-"; /* arrow pointing up */ +ACS_BOARD :: #run A_ALTCHARSET | #char "h"; /* board of squares */ +ACS_LANTERN :: #run A_ALTCHARSET | #char "i"; /* lantern symbol */ +ACS_BLOCK :: #run A_ALTCHARSET | #char "0"; /* solid square block */ +// These aren't documented, but a lot of System Vs have them anyway +// (you can spot pprryyzz{{||}} in a lot of AT&T terminfo strings). +// The ACS_names may not match AT&T's, our source didn't know them. +ACS_S3 :: #run A_ALTCHARSET | #char "p"; /* scan line 3 */ +ACS_S7 :: #run A_ALTCHARSET | #char "r"; /* scan line 7 */ +ACS_LEQUAL :: #run A_ALTCHARSET | #char "y"; /* less/equal */ +ACS_GEQUAL :: #run A_ALTCHARSET | #char "z"; /* greater/equal */ +ACS_PI :: #run A_ALTCHARSET | #char "{"; /* Pi */ +ACS_NEQUAL :: #run A_ALTCHARSET | #char "|"; /* not equal */ +ACS_STERLING :: #run A_ALTCHARSET | #char "}"; /* UK pound sign */ + + +COLOR_PAIR :: (n: s32) -> s32 #foreign ncurses; + +COLOR_BLACK :: 0; +COLOR_RED :: 1; +COLOR_GREEN :: 2; +COLOR_YELLOW :: 3; +COLOR_BLUE :: 4; +COLOR_MAGENTA :: 5; +COLOR_CYAN :: 6; +COLOR_WHITE :: 7; + +ERR :: -1; +OK :: 0; + + +KEY_CODE_YES :: 0400; +KEY_MIN :: 0401; +KEY_BREAK :: 0401; +KEY_SRESET :: 0530; +KEY_RESET :: 0531; +KEY_DOWN :: 0402; +KEY_UP :: 0403; +KEY_LEFT :: 0404; +KEY_RIGHT :: 0405; +KEY_HOME :: 0406; +KEY_BACKSPACE :: 0407; +KEY_F0 :: 0410; +KEY_F :: inline (n: s32) -> s32 { return KEY_F0+n; }; +KEY_DL :: 0510; +KEY_IL :: 0511; +KEY_DC :: 0512; +KEY_IC :: 0513; +KEY_EIC :: 0514; +KEY_CLEAR :: 0515; +KEY_EOS :: 0516; +KEY_EOL :: 0517; +KEY_SF :: 0520; +KEY_SR :: 0521; +KEY_NPAGE :: 0522; +KEY_PPAGE :: 0523; +KEY_STAB :: 0524; +KEY_CTAB :: 0525; +KEY_CATAB :: 0526; +KEY_ENTER :: 0527; +KEY_PRINT :: 0532; +KEY_LL :: 0533; +KEY_A1 :: 0534; +KEY_A3 :: 0535; +KEY_B2 :: 0536; +KEY_C1 :: 0537; +KEY_C3 :: 0540; +KEY_BTAB :: 0541; +KEY_BEG :: 0542; +KEY_CANCEL :: 0543; +KEY_CLOSE :: 0544; +KEY_COMMAND :: 0545; +KEY_COPY :: 0546; +KEY_CREATE :: 0547; +KEY_END :: 0550; +KEY_EXIT :: 0551; +KEY_FIND :: 0552; +KEY_HELP :: 0553; +KEY_MARK :: 0554; +KEY_MESSAGE :: 0555; +KEY_MOVE :: 0556; +KEY_NEXT :: 0557; +KEY_OPEN :: 0560; +KEY_OPTIONS :: 0561; +KEY_PREVIOUS :: 0562; +KEY_REDO :: 0563; +KEY_REFERENCE :: 0564; +KEY_REFRESH :: 0565; +KEY_REPLACE :: 0566; +KEY_RESTART :: 0567; +KEY_RESUME :: 0570; +KEY_SAVE :: 0571; +KEY_SBEG :: 0572; +KEY_SCANCEL :: 0573; +KEY_SCOMMAND :: 0574; +KEY_SCOPY :: 0575; +KEY_SCREATE :: 0576; +KEY_SDC :: 0577; +KEY_SDL :: 0600; +KEY_SELECT :: 0601; +KEY_SEND :: 0602; +KEY_SEOL :: 0603; +KEY_SEXIT :: 0604; +KEY_SFIND :: 0605; +KEY_SHELP :: 0606; +KEY_SHOME :: 0607; +KEY_SIC :: 0610; +KEY_SLEFT :: 0611; +KEY_SMESSAGE :: 0612; +KEY_SMOVE :: 0613; +KEY_SNEXT :: 0614; +KEY_SOPTIONS :: 0615; +KEY_SPREVIOUS :: 0616; +KEY_SPRINT :: 0617; +KEY_SREDO :: 0620; +KEY_SREPLACE :: 0621; +KEY_SRIGHT :: 0622; +KEY_SRSUME :: 0623; +KEY_SSAVE :: 0624; +KEY_SSUSPEND :: 0625; +KEY_SUNDO :: 0626; +KEY_SUSPEND :: 0627; +KEY_UNDO :: 0630; +KEY_MOUSE :: 0631; +KEY_RESIZE :: 0632; +KEY_MAX :: 0777; diff --git a/sizeof.c b/sizeof.c new file mode 100644 index 0000000..4844cba --- /dev/null +++ b/sizeof.c @@ -0,0 +1,47 @@ +// compile with : gcc sizeof.c -lncurses + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + initscr(); + + fprintf(stderr, "sizeof char: %d\n", sizeof(char)); + fprintf(stderr, "sizeof short: %d\n", sizeof(short)); + fprintf(stderr, "sizeof int: %d\n", sizeof(int)); + fprintf(stderr, "sizeof unsigned: %d\n", sizeof(unsigned)); + fprintf(stderr, "sizeof chtype: %d\n", sizeof(chtype)); + int w_size_x, w_size_y; + getmaxyx(stdscr, w_size_y, w_size_x); + char str[64]; + memset(str, 0, 64); + sprintf(str, "x,y : %dx%d\n", w_size_x, w_size_y); + mvaddstr(2, 2, str); + + unsigned m = ACS_DIAMOND; + fprintf(stderr, "sizeof ACS %d\n", sizeof(ACS_DIAMOND)); + + if (ACS_DIAMOND != 0 || ACS_URCORNER != 0){ + fprintf(stderr, "BAZINGA\n"); + } +// fprintf(stderr, ">%d<\n", strlen(ACS_DIAMOND)); + + mvaddch(0, 0, m); + getch(); + endwin(); +} + diff --git a/ttt.jai b/ttt.jai index 96a5fdb..b5fb59d 100644 --- a/ttt.jai +++ b/ttt.jai @@ -74,20 +74,23 @@ Database :: struct { database : Database; archive : Database; is_autosave_enabled := true; -// int countdown_to_autosave = -1; +countdown_to_autosave := -1; app_directory : string; db_file_path : string; ar_file_path : string; // char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors. // size_t string_buffer_size = 0; -// int size_x, size_y, pos_x, pos_y; +size_x : s32; +size_y : s32; +pos_x : s32; +pos_y : s32; Styles :: enum s16 { STYLE_SELECTED :: 1; - STYLE_SELECTED_INVERTED; + SELECTED_INVERTED; STYLE_ACTIVE; STYLE_ACTIVE_SELECTED; - STYLE_ERROR; + ERROR; } Layouts :: enum u8 { @@ -109,7 +112,7 @@ print_error :: (format :string, args : .. Any) { // int w_size_y = 4; // if (error_window == NULL) { // error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); -// wattron(error_window, COLOR_PAIR(STYLE_ERROR)); +// wattron(error_window, COLOR_PAIR(ERROR)); // wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); // mvwprintw(error_window, 0, 1, " Error "); // wmove(error_window, 1, 0); @@ -152,18 +155,15 @@ draw_error_window :: () { */ } -/* -void trigger_autosave() { +trigger_autosave :: () { countdown_to_autosave = 13375; // ms } -void show_processing() { +show_processing :: () { mvaddch(0, 0, ACS_DIAMOND); refresh(); } -*/ - // 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; @@ -299,29 +299,24 @@ sub_int64 :: (x :s64, y :s64) -> s64 { x - y; } -/* // Returns active task or NULL if none applies. -task_st *get_active_task(database_st *db) { - assert(db != NULL); - - task_st *task = NULL; - if (db->active_task >= 0) { - task = db->tasks + db->active_task; +get_active_task :: inline (db: Database) -> *Task { + task: *Task = null; + if (db.active_idx >= 0) { + task = *db.tasks[db.active_idx]; } return task; } // Returns selected task or NULL if none applies. -task_st *get_selected_task(database_st *db) { - assert(db != NULL); - - task_st *task = NULL; - if (db->selected_task >= 0) { - task = db->tasks + db->selected_task; +get_selected_task :: inline (db: Database) -> *Task { + task: *Task = null; + if (db.selected_idx >= 0) { + task = *db.tasks[db.selected_idx]; } return task; } -*/ + // Adds a task to the database. // If necessary, expands database capacity. // Returns success. @@ -528,11 +523,12 @@ void move_task_to_index(database_st *db, task_st *task, ptrdiff_t index) { } db->selected_task = target_index; } - +*/ // Updates the times on the active task (and adjusts database totals). -void update_times(database_st *db) { - assert(db != NULL); - +update_times :: (db: *Database) { + assert(db != null); + return; + /* // Get current UTC time. time_t stop_time = time(NULL); @@ -566,8 +562,9 @@ void update_times(database_st *db) { start_time = next_start; } + */ } - +/* // Recalculates database totals. void update_total_times(database_st *db) { assert(db != NULL); @@ -642,8 +639,7 @@ reset_database :: (db: *Database) { // Stores data from database into binary file. // Returns success. -store_database :: (db: *Database, path: string) -> success: bool { - assert(db != null, "Parameter 'db' is null."); +store_database :: (db: Database, path: string) -> success: bool { assert(xx path, "Parameter 'path' is empty."); // Open file. @@ -655,7 +651,7 @@ store_database :: (db: *Database, path: string) -> success: bool { defer file_close(*file); file_write(*file, DB_FILE_SIGN_STR); - file_write(*file, db, size_of(Database)); + file_write(*file, *db, size_of(Database)); file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); return true; @@ -710,8 +706,7 @@ load_database :: (db: *Database, path: string) -> success: bool { // Exports data into CSV file. // Returns success. -export_to_csv :: (db: *Database, path: string) -> success: bool { - assert(db != null, "Parameter 'db' is null."); +export_to_csv :: (db: Database, path: string) -> success: bool { assert(xx path, "Parameter 'path' is empty."); // TODO Make sure (IN ALL PROCEDURES) we're not receiving an empty path. @@ -1013,7 +1008,7 @@ initialize_tui :: () { // TODO //setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. - stdscr = initscr(); // Start curses mode. + stdscr = initscr(); // Start curses mode. cbreak(); // Line buffering disabled; pass on everty thing to me. keypad(stdscr, true); // I need those nifty F1..F12. curs_set(0); // Set cursor invisible. @@ -1023,29 +1018,28 @@ initialize_tui :: () { start_color(); use_default_colors(); // Using default (-1) instead of COLOR_BLACK. init_pair(xx Styles.STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN); - init_pair(xx Styles.STYLE_SELECTED_INVERTED, COLOR_CYAN, -1); + init_pair(xx Styles.SELECTED_INVERTED, COLOR_CYAN, -1); init_pair(xx Styles.STYLE_ACTIVE, COLOR_BLUE, -1); init_pair(xx Styles.STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); - init_pair(xx Styles.STYLE_ERROR, COLOR_RED, -1); + init_pair(xx Styles.ERROR, COLOR_RED, -1); } -/* -void update_layout() { +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_st *layout = layouts; layout <= &layouts[NUM_LAYOUTS - 1]; layout++) { - layout->columns[0].width = size_x - (NUM_COLUMNS - 1) - 2; - for (int idx = 1; idx < NUM_COLUMNS; idx++) { - layout->columns[0].width -= layout->columns[idx].width; + 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; } } } -void draw_tui(database_st *db, layout_st *layout) { - - const static int adjust_first_day_of_week[] = { +draw_tui :: (db: *Database, layout: *Layout) { + + adjust_first_day_of_week := int.[ (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (2 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, @@ -1053,36 +1047,37 @@ void draw_tui(database_st *db, layout_st *layout) { (4 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (5 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (6 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - }; - - int x, y; - column_st *col; + ]; + x: int; + y: int; + col: *Column; + // Get context information. - task_st *active_task = get_active_task(db); - task_st *selected_task = get_selected_task(db); - time_t now_utc = time(NULL); - int now_week_day = localtime(&now_utc)->tm_wday; + active_task := get_active_task(db); + selected_task := get_selected_task(db); + now_utc := current_time_consensus(); + now_week_day := to_calendar(now_utc).day_of_week_starting_at_0; // Reset theme and clear screen. attrset(A_NORMAL); erase(); - + // Draw outer border. box(stdscr, 0, 0); - + // Draw table grids. y = 0; x = 0; - for (int idx = 0; idx < NUM_COLUMNS - 1; idx++) { - x += 1 + layout->columns[idx].width; - mvaddch(y, x, ACS_TTEE); - for (y = 1; y < size_y - 1; y++) { - mvaddch(y, x, ACS_VLINE); + for layout.columns { + x += 1 + it.width; + mvaddch(xx y, xx x, ACS_TTEE); + for row: 1..size_y-1 { + mvaddch(xx row, xx x, ACS_VLINE); } - mvaddch(size_y - 1, x, ACS_BTEE); + mvaddch(size_y-1, xx x, ACS_BTEE); } - +return; /* /////////////////////////////////////////////////////////////////////////// // Draw headers. @@ -1105,7 +1100,7 @@ void draw_tui(database_st *db, layout_st *layout) { attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); } else if (idx == now_week_day) { - attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD); + attron(COLOR_PAIR(SELECTED_INVERTED) | A_BOLD); } col = &layout->columns[L_DAYS_IDX + idx]; @@ -1203,7 +1198,7 @@ void draw_tui(database_st *db, layout_st *layout) { attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); } else if (idx == now_week_day) { - attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD); + attron(COLOR_PAIR(SELECTED_INVERTED) | A_BOLD); } column_width = layout->columns[L_DAYS_IDX + idx].width; @@ -1216,8 +1211,9 @@ void draw_tui(database_st *db, layout_st *layout) { } x++; mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width); + */ } - +/* void *mem_alloc(size_t mem_size, const char *error_tag) { void *mem_pointer = malloc(mem_size); if (mem_pointer == NULL && mem_size > 0) { @@ -1335,14 +1331,14 @@ main :: () { { // Initialize database and archive files if needed. if (file_exists(db_file_path) == false) { - if (store_database(*database, db_file_path) == false) { + if (store_database(database, db_file_path) == false) { print_error("Failed to initialize database."); exit(1); } } if (file_exists(ar_file_path) == false) { - if (export_to_csv(*archive, ar_file_path) == false) { + if (export_to_csv(archive, ar_file_path) == false) { print_error("Failed to initialize archive."); exit(1); } @@ -1474,67 +1470,69 @@ main :: () { } initialize_tui(); - /* + // TODO Remove this?! //signal(SIGTERM, exit_gracefully); //signal(SIGINT, exit_gracefully); //signal(SIGQUIT, exit_gracefully); //signal(SIGHUP, exit_gracefully); + layout := *layouts[Layouts.COMPACT]; + db := *database; + flushinp(); ungetch(KEY_RESIZE); - for (int key; ((key = getch()) != 'q') && (key != 'Q'); ) { + while (true) { + key := getch(); + if key == #char "q" || key == #char "Q" break; - static layout_st *layout = &layouts[L_COMPACT]; - task_st *active_task = get_active_task(db); - task_st *selected_task = get_selected_task(db); - int action_style = A_BOLD | COLOR_PAIR(selected_task == active_task && selected_task != NULL ? STYLE_ACTIVE : STYLE_SELECTED_INVERTED); - int error_style = A_BOLD | COLOR_PAIR(STYLE_ERROR); - int selected_task_row = is_terminal_too_small ? 0 - : (db->selected_task < 0) ? 1 - : (db->selected_task % layout_tasks_rows) + NUM_HEADER_ROWS; + active_task := get_active_task(db); + selected_task := get_selected_task(db); + action_style := A_BOLD | COLOR_PAIR(xx ifx selected_task == active_task && selected_task != null + then Styles.STYLE_ACTIVE + else Styles.SELECTED_INVERTED); + error_style := A_BOLD | COLOR_PAIR(xx Styles.ERROR); + selected_task_row : int = ifx is_terminal_too_small then 0 + else ifx (db.selected_idx < 0) then 1 + else (db.selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS; timeout(INPUT_AWAIT_INF); - update_times(&database); + update_times(*database); - switch(key) { + if key == { // When getch() times out. - case ERR: { + case ERR; if (is_autosave_enabled && countdown_to_autosave > 0) { countdown_to_autosave -= INPUT_TIMEOUT_MS; if (countdown_to_autosave <= 0) { show_processing(); - if (db == &archive) { - export_to_csv(&archive, ar_file_path); + if (db == *archive) { + export_to_csv(*archive, ar_file_path); } - store_database(&database, db_file_path); + store_database(database, db_file_path); } } - break; - } // When terminal is resized. - case KEY_RESIZE: { + case KEY_RESIZE; // BUG KEY_RESIZE is not happening. clear(); - getmaxyx(stdscr, size_y, size_x); + getmaxyx(stdscr, *size_y, *size_x); is_terminal_too_small = size_x < 60 || size_y < 3; - size_t new_size = 2047 | TASK_NAME_BYTES | (size_x + 1); - if (string_buffer_size < new_size) { - string_buffer_size = new_size; - string_buffer = realloc(string_buffer, string_buffer_size); - if (string_buffer == NULL && string_buffer_size > 0) { - print_error("Failed to allocate memory for string buffer: %s.", strerror(errno)); - flushinp(); - ungetch('q'); - break; - } - } + new_size := 2047 | TASK_NAME_BYTES | (size_x + 1); + //if (string_buffer_size < new_size) { + //string_buffer_size = new_size; + //string_buffer = realloc(string_buffer, string_buffer_size); + //if (string_buffer == NULL && string_buffer_size > 0) { + //print_error("Failed to allocate memory for string buffer: %s.", strerror(errno)); + //flushinp(); + //ungetch(#char "q"); + //break; + //} + //} update_layout(); - layout = &layouts[size_x > 100 ? L_NORMAL : L_COMPACT]; - break; - } - + layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; +/* case 'n': case 'N':{ if (is_database_full(db)) { @@ -1849,12 +1847,12 @@ main :: () { select_task_by_delta(db, layout_tasks_rows); break; } +*/ } if (is_terminal_too_small) { - const char *INVALID_WINDOW_MESSAGE = "Terminal is too small: minimum 60x3."; - const int INVALID_WINDOW_MESSAGE_LENGTH = strlen(INVALID_WINDOW_MESSAGE); - mvaddstr(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE_LENGTH) / 2, INVALID_WINDOW_MESSAGE); + INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; + mvaddstr(size_y / 2, (size_x - xx INVALID_WINDOW_MESSAGE.count) / 2, INVALID_WINDOW_MESSAGE); } else { draw_tui(db, layout); @@ -1862,8 +1860,9 @@ main :: () { } timeout(INPUT_TIMEOUT_MS); + } - */ + // Save any unsaved changes. // show_processing(); error_saving := false; -- cgit v1.2.3 From 27fc71a32cc474d7e657d2731e2f2308c9a4d8e3 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 4 Apr 2023 22:51:28 +0100 Subject: Fixed KEY_* values which were octal but I assumed decimals. --- curses.jai | 190 ++++++++++++++++++++++++++++++------------------------------- sizeof.c | 2 + ttt.jai | 8 +-- 3 files changed, 100 insertions(+), 100 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index 532054a..ef9ff96 100644 --- a/curses.jai +++ b/curses.jai @@ -179,7 +179,6 @@ ACS_PI :: #run A_ALTCHARSET | #char "{"; /* Pi */ ACS_NEQUAL :: #run A_ALTCHARSET | #char "|"; /* not equal */ ACS_STERLING :: #run A_ALTCHARSET | #char "}"; /* UK pound sign */ - COLOR_PAIR :: (n: s32) -> s32 #foreign ncurses; COLOR_BLACK :: 0; @@ -194,99 +193,98 @@ COLOR_WHITE :: 7; ERR :: -1; OK :: 0; - -KEY_CODE_YES :: 0400; -KEY_MIN :: 0401; -KEY_BREAK :: 0401; -KEY_SRESET :: 0530; -KEY_RESET :: 0531; -KEY_DOWN :: 0402; -KEY_UP :: 0403; -KEY_LEFT :: 0404; -KEY_RIGHT :: 0405; -KEY_HOME :: 0406; -KEY_BACKSPACE :: 0407; -KEY_F0 :: 0410; +KEY_CODE_YES :: 256; +KEY_MIN :: 257; +KEY_BREAK :: 257; +KEY_SRESET :: 344; +KEY_RESET :: 345; +KEY_DOWN :: 258; +KEY_UP :: 259; +KEY_LEFT :: 260; +KEY_RIGHT :: 261; +KEY_HOME :: 262; +KEY_BACKSPACE :: 263; +KEY_F0 :: 264; KEY_F :: inline (n: s32) -> s32 { return KEY_F0+n; }; -KEY_DL :: 0510; -KEY_IL :: 0511; -KEY_DC :: 0512; -KEY_IC :: 0513; -KEY_EIC :: 0514; -KEY_CLEAR :: 0515; -KEY_EOS :: 0516; -KEY_EOL :: 0517; -KEY_SF :: 0520; -KEY_SR :: 0521; -KEY_NPAGE :: 0522; -KEY_PPAGE :: 0523; -KEY_STAB :: 0524; -KEY_CTAB :: 0525; -KEY_CATAB :: 0526; -KEY_ENTER :: 0527; -KEY_PRINT :: 0532; -KEY_LL :: 0533; -KEY_A1 :: 0534; -KEY_A3 :: 0535; -KEY_B2 :: 0536; -KEY_C1 :: 0537; -KEY_C3 :: 0540; -KEY_BTAB :: 0541; -KEY_BEG :: 0542; -KEY_CANCEL :: 0543; -KEY_CLOSE :: 0544; -KEY_COMMAND :: 0545; -KEY_COPY :: 0546; -KEY_CREATE :: 0547; -KEY_END :: 0550; -KEY_EXIT :: 0551; -KEY_FIND :: 0552; -KEY_HELP :: 0553; -KEY_MARK :: 0554; -KEY_MESSAGE :: 0555; -KEY_MOVE :: 0556; -KEY_NEXT :: 0557; -KEY_OPEN :: 0560; -KEY_OPTIONS :: 0561; -KEY_PREVIOUS :: 0562; -KEY_REDO :: 0563; -KEY_REFERENCE :: 0564; -KEY_REFRESH :: 0565; -KEY_REPLACE :: 0566; -KEY_RESTART :: 0567; -KEY_RESUME :: 0570; -KEY_SAVE :: 0571; -KEY_SBEG :: 0572; -KEY_SCANCEL :: 0573; -KEY_SCOMMAND :: 0574; -KEY_SCOPY :: 0575; -KEY_SCREATE :: 0576; -KEY_SDC :: 0577; -KEY_SDL :: 0600; -KEY_SELECT :: 0601; -KEY_SEND :: 0602; -KEY_SEOL :: 0603; -KEY_SEXIT :: 0604; -KEY_SFIND :: 0605; -KEY_SHELP :: 0606; -KEY_SHOME :: 0607; -KEY_SIC :: 0610; -KEY_SLEFT :: 0611; -KEY_SMESSAGE :: 0612; -KEY_SMOVE :: 0613; -KEY_SNEXT :: 0614; -KEY_SOPTIONS :: 0615; -KEY_SPREVIOUS :: 0616; -KEY_SPRINT :: 0617; -KEY_SREDO :: 0620; -KEY_SREPLACE :: 0621; -KEY_SRIGHT :: 0622; -KEY_SRSUME :: 0623; -KEY_SSAVE :: 0624; -KEY_SSUSPEND :: 0625; -KEY_SUNDO :: 0626; -KEY_SUSPEND :: 0627; -KEY_UNDO :: 0630; -KEY_MOUSE :: 0631; -KEY_RESIZE :: 0632; -KEY_MAX :: 0777; +KEY_DL :: 328; +KEY_IL :: 329; +KEY_DC :: 330; +KEY_IC :: 331; +KEY_EIC :: 332; +KEY_CLEAR :: 333; +KEY_EOS :: 334; +KEY_EOL :: 335; +KEY_SF :: 336; +KEY_SR :: 337; +KEY_NPAGE :: 338; +KEY_PPAGE :: 339; +KEY_STAB :: 340; +KEY_CTAB :: 341; +KEY_CATAB :: 342; +KEY_ENTER :: 343; +KEY_PRINT :: 346; +KEY_LL :: 347; +KEY_A1 :: 348; +KEY_A3 :: 349; +KEY_B2 :: 350; +KEY_C1 :: 351; +KEY_C3 :: 352; +KEY_BTAB :: 353; +KEY_BEG :: 354; +KEY_CANCEL :: 355; +KEY_CLOSE :: 356; +KEY_COMMAND :: 357; +KEY_COPY :: 358; +KEY_CREATE :: 359; +KEY_END :: 360; +KEY_EXIT :: 361; +KEY_FIND :: 362; +KEY_HELP :: 363; +KEY_MARK :: 364; +KEY_MESSAGE :: 365; +KEY_MOVE :: 366; +KEY_NEXT :: 367; +KEY_OPEN :: 368; +KEY_OPTIONS :: 369; +KEY_PREVIOUS :: 370; +KEY_REDO :: 371; +KEY_REFERENCE :: 372; +KEY_REFRESH :: 373; +KEY_REPLACE :: 374; +KEY_RESTART :: 375; +KEY_RESUME :: 376; +KEY_SAVE :: 377; +KEY_SBEG :: 378; +KEY_SCANCEL :: 379; +KEY_SCOMMAND :: 380; +KEY_SCOPY :: 381; +KEY_SCREATE :: 382; +KEY_SDC :: 383; +KEY_SDL :: 384; +KEY_SELECT :: 385; +KEY_SEND :: 386; +KEY_SEOL :: 387; +KEY_SEXIT :: 388; +KEY_SFIND :: 389; +KEY_SHELP :: 390; +KEY_SHOME :: 391; +KEY_SIC :: 392; +KEY_SLEFT :: 393; +KEY_SMESSAGE :: 394; +KEY_SMOVE :: 395; +KEY_SNEXT :: 396; +KEY_SOPTIONS :: 397; +KEY_SPREVIOUS :: 398; +KEY_SPRINT :: 399; +KEY_SREDO :: 400; +KEY_SREPLACE :: 401; +KEY_SRIGHT :: 402; +KEY_SRSUME :: 403; +KEY_SSAVE :: 404; +KEY_SSUSPEND :: 405; +KEY_SUNDO :: 406; +KEY_SUSPEND :: 407; +KEY_UNDO :: 408; +KEY_MOUSE :: 409; +KEY_RESIZE :: 410; +KEY_MAX :: 511; diff --git a/sizeof.c b/sizeof.c index 4844cba..9025f78 100644 --- a/sizeof.c +++ b/sizeof.c @@ -31,6 +31,8 @@ int main(int argc, char **argv) { memset(str, 0, 64); sprintf(str, "x,y : %dx%d\n", w_size_x, w_size_y); mvaddstr(2, 2, str); + sprintf(str, "resize:%d\n", KEY_RESIZE); + mvaddstr(3, 2, str); unsigned m = ACS_DIAMOND; fprintf(stderr, "sizeof ACS %d\n", sizeof(ACS_DIAMOND)); diff --git a/ttt.jai b/ttt.jai index b5fb59d..8f4b801 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1069,8 +1069,9 @@ draw_tui :: (db: *Database, layout: *Layout) { // Draw table grids. y = 0; x = 0; - for layout.columns { - x += 1 + it.width; + for 0..layout.columns.count-2 { + column := layout.columns[it]; + x += 1 + column.width; mvaddch(xx y, xx x, ACS_TTEE); for row: 1..size_y-1 { mvaddch(xx row, xx x, ACS_VLINE); @@ -1515,7 +1516,7 @@ main :: () { } // When terminal is resized. - case KEY_RESIZE; // BUG KEY_RESIZE is not happening. + case KEY_RESIZE; clear(); getmaxyx(stdscr, *size_y, *size_x); is_terminal_too_small = size_x < 60 || size_y < 3; @@ -1889,7 +1890,6 @@ main :: () { exit(xx ifx error_saving then 1 else 0); } - /* main :: () { -- cgit v1.2.3 From 78dc3662e80c63775ee61cfa0cb7cc74fa76bd98 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 4 Apr 2023 23:29:08 +0100 Subject: Stuck on mvprintw. --- curses.jai | 2 ++ ttt.jai | 96 ++++++++++++++++++++++++++++++++------------------------------ 2 files changed, 52 insertions(+), 46 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index ef9ff96..87fb1dc 100644 --- a/curses.jai +++ b/curses.jai @@ -93,9 +93,11 @@ keypad :: (win: *WINDOW, bf: bool) -> s32 #foreign ncurses ungetch :: (ch: s32) -> s32 #foreign ncurses; attrset :: (attrs: s32) -> s32 #foreign ncurses; +attron :: (attrs: s32) -> s32 #foreign ncurses; erase :: () -> s32 #foreign ncurses; curs_set :: (visibility: s32) -> s32 #foreign ncurses; mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; +mvprintw :: (y: s32, x: s32, fmt: *u8, ...) -> s32 #foreign ncurses; noecho :: () -> s32 #foreign ncurses; box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; diff --git a/ttt.jai b/ttt.jai index 8f4b801..9a5f26a 100644 --- a/ttt.jai +++ b/ttt.jai @@ -86,10 +86,10 @@ pos_x : s32; pos_y : s32; Styles :: enum s16 { - STYLE_SELECTED :: 1; + SELECTED :: 1; SELECTED_INVERTED; - STYLE_ACTIVE; - STYLE_ACTIVE_SELECTED; + ACTIVE; + ACTIVE_SELECTED; ERROR; } @@ -1017,10 +1017,10 @@ initialize_tui :: () { // Initialize pairs of colors. start_color(); use_default_colors(); // Using default (-1) instead of COLOR_BLACK. - init_pair(xx Styles.STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN); + init_pair(xx Styles.SELECTED, COLOR_BLACK, COLOR_CYAN); init_pair(xx Styles.SELECTED_INVERTED, COLOR_CYAN, -1); - init_pair(xx Styles.STYLE_ACTIVE, COLOR_BLUE, -1); - init_pair(xx Styles.STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); + init_pair(xx Styles.ACTIVE, COLOR_BLUE, -1); + init_pair(xx Styles.ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); init_pair(xx Styles.ERROR, COLOR_RED, -1); } @@ -1067,6 +1067,7 @@ draw_tui :: (db: *Database, layout: *Layout) { box(stdscr, 0, 0); // Draw table grids. + // TODO Maybe this could be simplified? y = 0; x = 0; for 0..layout.columns.count-2 { @@ -1078,7 +1079,7 @@ draw_tui :: (db: *Database, layout: *Layout) { } mvaddch(size_y-1, xx x, ACS_BTEE); } -return; /* + /////////////////////////////////////////////////////////////////////////// // Draw headers. @@ -1086,94 +1087,97 @@ return; /* x = 0; // Headers : title - x++; - col = &layout->columns[L_TITLE_IDX]; - mvaddstr(y, x + col->alignment_offset, (db == &archive ? layout->archive_title : col->header)); - x += col->width; + x += 1; + col = *layout.columns[L_TITLE_IDX]; + mvaddstr(xx y, xx (x + col.alignment_offset), ifx db == *archive then layout.archive_title.data else col.header.data); + x += col.width; // Headers : days - for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) { - int idx = adjust_first_day_of_week[raw_idx]; - x++; + for 0..NUM_WEEK_DAYS-1 { + //for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) { + idx := adjust_first_day_of_week[it]; + x += 1; // Apply theme. - if (idx == now_week_day && active_task != NULL) { - attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); + if (idx == now_week_day && active_task != null) { + attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); } else if (idx == now_week_day) { - attron(COLOR_PAIR(SELECTED_INVERTED) | A_BOLD); + attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); } - col = &layout->columns[L_DAYS_IDX + idx]; - mvaddstr(y, x + col->alignment_offset, col->header); - x += col->width; + col = *layout.columns[L_DAYS_IDX + idx]; + mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); + x += col.width; // Reset theme. attrset(A_NORMAL); } // Headers : total - x++; - col = &layout->columns[L_TOTAL_IDX]; - mvaddstr(y, x + col->alignment_offset, col->header); + x += 1; + col = *layout.columns[L_TOTAL_IDX]; + mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); /////////////////////////////////////////////////////////////////////////// // Draw tasks. - uint64_t total_time = 0; - int column_width; + total_time := 0; + column_width: int; y = 0; // Pagination based on currently selected task (show page where selected task is). - size_t idx_start = (db->selected_task / layout_tasks_rows) * layout_tasks_rows; + idx_start := (db.selected_idx / layout_tasks_rows) * layout_tasks_rows; // Display up to rows allowed by the layout, or less if reached end of database. - size_t idx_stop = idx_start + (layout_tasks_rows > db->count - idx_start ? db->count - idx_start : layout_tasks_rows); - for (size_t idx = idx_start; idx < idx_stop; idx++) { - task_st *task = &db->tasks[idx]; - y++; + idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); + for idx: idx_start..idx_stop { + //for (size_t idx = idx_start; idx < idx_stop; idx++) { + task := *db.tasks[idx]; + y += 1; x = 0; // Apply theme. if (task == active_task && task == selected_task) { - attron(COLOR_PAIR(STYLE_ACTIVE_SELECTED) | A_BOLD); + attron(COLOR_PAIR(xx Styles.ACTIVE_SELECTED) | A_BOLD); } else if (task == selected_task) { - attron(COLOR_PAIR(STYLE_SELECTED)); + attron(COLOR_PAIR(xx Styles.SELECTED)); } else if (task == active_task) { - attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); + attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); } // Task title. - x++; - column_width = layout->columns[L_TITLE_IDX].width; - mvprintw(y, x, "%-*.*s", column_width, column_width, task->name); + x += 1; + column_width = layout.columns[L_TITLE_IDX].width; + mvprintw(y, x, "%-*.*s", column_width, column_width, task.name); x += column_width; // Task times. total_time = 0; - for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { - x++; + for 0..NUM_WEEK_DAYS-1 { + //for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + x += 1; - int day_idx = (idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + day_idx := (idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; - column_width = layout->columns[L_DAYS_IDX + day_idx].width; - int64_t task_stime = task->times[day_idx]; + column_width = layout.columns[L_DAYS_IDX + day_idx].width; + task_stime := task.times[day_idx]; total_time = add_int64(total_time, task_stime); - mvprintw_time(y, x, task_stime, column_width); + mvprintw_time(xx y, xx x, task_stime, column_width); x += column_width; } // Task total. - x++; - mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width); + x += 1; + mvprintw_time(xx y, xx x, total_time, layout.columns[L_TOTAL_IDX].width); // Reset theme. attrset(A_NORMAL); } - + return; /* /////////////////////////////////////////////////////////////////////////// // Draw selected/total tasks. int size = snprintf(NULL, 0, " %td/%zd ", db->selected_task + 1, db->count); @@ -1490,7 +1494,7 @@ main :: () { active_task := get_active_task(db); selected_task := get_selected_task(db); action_style := A_BOLD | COLOR_PAIR(xx ifx selected_task == active_task && selected_task != null - then Styles.STYLE_ACTIVE + then Styles.ACTIVE else Styles.SELECTED_INVERTED); error_style := A_BOLD | COLOR_PAIR(xx Styles.ERROR); selected_task_row : int = ifx is_terminal_too_small then 0 -- cgit v1.2.3 From a1939b2707fea269bf6ba933b9dee60996aa6bc2 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 5 Apr 2023 18:29:31 +0100 Subject: Fixed variadic arguments on mvprintw binding. --- curses.jai | 2 +- ttt.jai | 46 +++++++++++++++++++++++----------------------- 2 files changed, 24 insertions(+), 24 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index 87fb1dc..f8715c0 100644 --- a/curses.jai +++ b/curses.jai @@ -97,7 +97,7 @@ attron :: (attrs: s32) -> s32 #foreign ncurses erase :: () -> s32 #foreign ncurses; curs_set :: (visibility: s32) -> s32 #foreign ncurses; mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; -mvprintw :: (y: s32, x: s32, fmt: *u8, ...) -> s32 #foreign ncurses; +mvprintw :: (y: s32, x: s32, fmt: *u8, args: ..Any) -> s32 #foreign ncurses; noecho :: () -> s32 #foreign ncurses; box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; diff --git a/ttt.jai b/ttt.jai index 9a5f26a..a5397ed 100644 --- a/ttt.jai +++ b/ttt.jai @@ -239,15 +239,16 @@ replace_char :: (str: string, find: u8, replace: u8) -> string { // TODO Use modules/String/module.jai:replace_chars return ""; } -/* + // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. -int mvprintw_time(int y, int x, intmax_t time, int space) { - const int TIME_CHARS = 5; +mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { + return 0; // TODO NOT WORKING + TIME_CHARS :: 5; assert(space >= TIME_CHARS); - int left_padding = (space - TIME_CHARS) / 2; - int right_padding = space - TIME_CHARS - left_padding; + left_padding := (space - TIME_CHARS) / 2; + right_padding := space - TIME_CHARS - left_padding; if (time < 0) { return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, ""); @@ -258,24 +259,24 @@ int mvprintw_time(int y, int x, intmax_t time, int space) { else if (time < SECONDS_IN_MINUTE) { return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); } - else if (time < (intmax_t)100 * SECONDS_IN_HOUR) { - intmax_t hours = (double)time / (double)SECONDS_IN_HOUR; - intmax_t minutes = (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE; + else if (time < 100 * SECONDS_IN_HOUR) { + hours := cast(float64)time / cast(float64)SECONDS_IN_HOUR; + minutes := (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE; return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); } - else if (time < (intmax_t)(9999.5 * SECONDS_IN_DAY)) { - double value = (double)time / (double)SECONDS_IN_DAY; - int decimals = - time >= 99.95 * SECONDS_IN_DAY ? 0 : - time >= 9.995 * SECONDS_IN_DAY ? 1 : + else if (time < xx (9999.5 * SECONDS_IN_DAY)) { + value := cast(float64)time / cast(float64)SECONDS_IN_DAY; + decimals := + ifx time >= xx 99.95 * SECONDS_IN_DAY then 0 else + ifx time >= xx 9.995 * SECONDS_IN_DAY then 1 else 2; return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); } - else if (time < (intmax_t)(9999.5 * SECONDS_IN_YEAR)) { - double value = (double)time / (double)SECONDS_IN_YEAR; - int decimals = - time >= 99.95 * SECONDS_IN_YEAR ? 0 : - time >= 9.995 * SECONDS_IN_YEAR ? 1 : + else if (time < xx (9999.5 * SECONDS_IN_YEAR)) { + value := cast(float64)time / cast(float64)SECONDS_IN_YEAR; + decimals := + ifx time >= xx 99.95 * SECONDS_IN_YEAR then 0 else + ifx time >= xx 9.995 * SECONDS_IN_YEAR then 1 else 2; return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); } @@ -283,7 +284,6 @@ int mvprintw_time(int y, int x, intmax_t time, int space) { return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); } } -*/ add_int64 :: (x :s64, y: s64) -> s64 { return @@ -1131,7 +1131,7 @@ draw_tui :: (db: *Database, layout: *Layout) { idx_start := (db.selected_idx / layout_tasks_rows) * layout_tasks_rows; // Display up to rows allowed by the layout, or less if reached end of database. idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); - for idx: idx_start..idx_stop { + for idx: idx_start..idx_stop-1 { //for (size_t idx = idx_start; idx < idx_stop; idx++) { task := *db.tasks[idx]; y += 1; @@ -1151,7 +1151,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Task title. x += 1; column_width = layout.columns[L_TITLE_IDX].width; - mvprintw(y, x, "%-*.*s", column_width, column_width, task.name); + mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, task.name); x += column_width; // Task times. @@ -1165,13 +1165,13 @@ draw_tui :: (db: *Database, layout: *Layout) { column_width = layout.columns[L_DAYS_IDX + day_idx].width; task_stime := task.times[day_idx]; total_time = add_int64(total_time, task_stime); - mvprintw_time(xx y, xx x, task_stime, column_width); + mvprintw_time(xx y, xx x, task_stime, xx column_width); x += column_width; } // Task total. x += 1; - mvprintw_time(xx y, xx x, total_time, layout.columns[L_TOTAL_IDX].width); + mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); // Reset theme. attrset(A_NORMAL); -- cgit v1.2.3 From 6703db54662b090d7c05f439e2e62e8ba8909911 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 5 Apr 2023 23:21:14 +0100 Subject: Fix bug on export_to_csv. --- ttt.jai | 1 + 1 file changed, 1 insertion(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index a5397ed..4d4421f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -720,6 +720,7 @@ export_to_csv :: (db: Database, path: string) -> success: bool { 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", -- cgit v1.2.3 From 9b207176f11e00fe0d3cff3ede331ef0b7954b40 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 00:34:02 +0100 Subject: Fixed mvprintw_time and ported select/active code. --- ttt.jai | 122 ++++++++++++++++++++++++++----------------------------------- unused.jai | 2 + 2 files changed, 54 insertions(+), 70 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 4d4421f..45a8bac 100644 --- a/ttt.jai +++ b/ttt.jai @@ -243,7 +243,6 @@ replace_char :: (str: string, find: u8, replace: u8) -> string { // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { - return 0; // TODO NOT WORKING TIME_CHARS :: 5; assert(space >= TIME_CHARS); @@ -260,12 +259,12 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); } else if (time < 100 * SECONDS_IN_HOUR) { - hours := cast(float64)time / cast(float64)SECONDS_IN_HOUR; + hours := time / SECONDS_IN_HOUR; minutes := (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE; return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); } else if (time < xx (9999.5 * SECONDS_IN_DAY)) { - value := cast(float64)time / cast(float64)SECONDS_IN_DAY; + value := cast(float64) time / SECONDS_IN_DAY; decimals := ifx time >= xx 99.95 * SECONDS_IN_DAY then 0 else ifx time >= xx 9.995 * SECONDS_IN_DAY then 1 else @@ -273,7 +272,7 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); } else if (time < xx (9999.5 * SECONDS_IN_YEAR)) { - value := cast(float64)time / cast(float64)SECONDS_IN_YEAR; + value := cast(float64) time / SECONDS_IN_YEAR; decimals := ifx time >= xx 99.95 * SECONDS_IN_YEAR then 0 else ifx time >= xx 9.995 * SECONDS_IN_YEAR then 1 else @@ -885,43 +884,45 @@ bool append_to_csv(task_st *task, const char *path) { fclose(file); return true; } - +*/ // Selects task by index. // Index gets clamped to [0, db->count[. -void select_task_by_index(database_st *db, ptrdiff_t index) { - assert(db != NULL); - db->selected_task = db->count == 0 ? -1 - : index < 0 ? 0 - : index >= db->count ? db->count - 1 - : index; +select_task_by_index :: (db: *Database, index: s64) { + assert(db != null); + db.selected_idx = ifx db.tasks.count == 0 then -1 else + ifx index < 0 then 0 else + ifx index >= db.tasks.count then db.tasks.count - 1 else + index; } // Selects task by delta relative to currently selected task. -void select_task_by_delta(database_st *db, ptrdiff_t delta) { - assert(db != NULL); - ptrdiff_t idx = (delta > 0 && db->selected_task > PTRDIFF_MAX - delta) ? PTRDIFF_MAX - : (delta < 0 && db->selected_task < PTRDIFF_MIN + delta) ? PTRDIFF_MIN - : db->selected_task + delta; +select_task_by_delta :: (db: *Database, delta: s64) { + assert(db != null); + // TODO I bet there's a better way to do this... maybe use a clamp or range or something. + 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_by_index(db, idx); } - +/* // Selects task. -void select_task(database_st *db, task_st *task) { - assert(db != NULL); - assert(task != NULL); - assert(task >= db->tasks && task - db->tasks < db->count); - db->selected_task = task - db->tasks; +select_task :: (db: *Database, task: *Task) { + assert(db != null); + assert(task != null); + assert(task >= db.tasks.data && task - db.tasks.data < db.tasks.count); + db.selected_idx = task - db.tasks.data; } - +*/ // Set active task. // Passing task as NULL de-activates any previously active task. -void set_active_task(database_st *db, task_st *task) { - assert(db != NULL); - assert(task == NULL || (task >= db->tasks && task < &db->tasks[db->count])); +set_active_task :: (db: *Database, task: *Task) { + assert(db != null); + assert(task == null || (task >= db.tasks.data && task <= *db.tasks[db.tasks.count-1])); // TODO Improve this check. update_times(db); - db->active_task = (task == NULL) ? -1 : task - db->tasks; + db.active_idx = ifx (task == null) then -1 else task - db.tasks.data; } - +/* // Returns true when database is full. bool is_database_full(database_st *db) { assert(db != NULL); @@ -1132,9 +1133,9 @@ draw_tui :: (db: *Database, layout: *Layout) { idx_start := (db.selected_idx / layout_tasks_rows) * layout_tasks_rows; // Display up to rows allowed by the layout, or less if reached end of database. idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); - for idx: idx_start..idx_stop-1 { + for task_idx: idx_start..idx_stop-1 { //for (size_t idx = idx_start; idx < idx_stop; idx++) { - task := *db.tasks[idx]; + task := *db.tasks[task_idx]; y += 1; x = 0; @@ -1158,15 +1159,12 @@ draw_tui :: (db: *Database, layout: *Layout) { // Task times. total_time = 0; for 0..NUM_WEEK_DAYS-1 { - //for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { x += 1; - - day_idx := (idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; - + day_idx := (it + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; column_width = layout.columns[L_DAYS_IDX + day_idx].width; - task_stime := task.times[day_idx]; - total_time = add_int64(total_time, task_stime); - mvprintw_time(xx y, xx x, task_stime, xx column_width); + task_time := task.times[day_idx]; + total_time = add_int64(total_time, task_time); + mvprintw_time(xx y, xx x, task_time, xx column_width); x += column_width; } @@ -1760,18 +1758,15 @@ main :: () { select_task(db, active_task); break; } - - case '\n': - case ' ': { - if (db != &database || selected_task == NULL) { - break; - } - set_active_task(db, (active_task == selected_task) ? NULL : selected_task); + */ + case #char "\n"; #through; + case #char " "; + if (db != *database || selected_task == null) break; + set_active_task(db, ifx (active_task == selected_task) then null else selected_task); active_task = get_active_task(db); trigger_autosave(); - break; - } - + + /* case '\t': { if (db == &database) { if (import_from_csv(&archive, ar_file_path) == false) { @@ -1823,37 +1818,24 @@ main :: () { trigger_autosave(); break; } - - case KEY_HOME: { + */ + case KEY_HOME; select_task_by_index(db, 0); - break; - } - - case KEY_UP: { + + case KEY_UP; select_task_by_delta(db, -1); - break; - } - - case KEY_PPAGE: { + + case KEY_PPAGE; select_task_by_delta(db, -layout_tasks_rows); - break; - } - case KEY_END: { - select_task_by_index(db, db->count-1); - break; - } + case KEY_END; + select_task_by_index(db, db.tasks.count-1); - case KEY_DOWN: { + case KEY_DOWN; select_task_by_delta(db, 1); - break; - } - case KEY_NPAGE: { + case KEY_NPAGE; select_task_by_delta(db, layout_tasks_rows); - break; - } -*/ } if (is_terminal_too_small) { diff --git a/unused.jai b/unused.jai index b995d0d..264fa88 100644 --- a/unused.jai +++ b/unused.jai @@ -1,3 +1,5 @@ + print(">%<", S64_MIN + delta, to_standard_error = true); + // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // -- cgit v1.2.3 From 4bd041428fc829891b18c6657b74824bfe4ee482 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 00:42:43 +0100 Subject: Add bug note. --- ttt.jai | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 45a8bac..9c2dcb9 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1096,11 +1096,10 @@ draw_tui :: (db: *Database, layout: *Layout) { // Headers : days for 0..NUM_WEEK_DAYS-1 { - //for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) { idx := adjust_first_day_of_week[it]; x += 1; - // Apply theme. + // Apply theme. FIXME Not working (tested at 00:40 UTC+1 and showed one day earlier. if (idx == now_week_day && active_task != null) { attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); } -- cgit v1.2.3 From 65468b9e712e77feb972b785328110a4e375d16b Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 15:58:16 +0100 Subject: Fixed bug: add times when importing CSV data. --- ttt.jai | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 9c2dcb9..d16efce 100644 --- a/ttt.jai +++ b/ttt.jai @@ -831,16 +831,18 @@ import_from_csv :: (db: *Database, path: string) -> bool { if success == false break; task: Task; - values := split(line, ","); // TODO Temporary memory... if file is too big this may break. + csv_values := split(line, ","); // TODO Temporary memory... if file is too big this may break. // Import task name. - name_length := min(task.name.count, values[0].count); - memcpy(task.name.data, values[0].data, name_length); + name_length := min(task.name.count, csv_values[0].count); + memcpy(task.name.data, csv_values[0].data, name_length); truncate_string(xx task.name, name_length); - advance(*values); - for values { - task.times[it_index] = string_to_int(it); + advance(*csv_values); + for csv_values { + parsed_value := string_to_int(it); + task.times[it_index] = parsed_value; + db.total_times[it_index] = add_int64(db.total_times[it_index], parsed_value); } add_task(db, *task); -- cgit v1.2.3 From 8016c2aabb20aa820eff09f339445a5524582c3d Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 15:58:42 +0100 Subject: Fixed bug: correct timezone is used on draw_tui. --- ttt.jai | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index d16efce..0955d09 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1061,7 +1061,7 @@ draw_tui :: (db: *Database, layout: *Layout) { active_task := get_active_task(db); selected_task := get_selected_task(db); now_utc := current_time_consensus(); - now_week_day := to_calendar(now_utc).day_of_week_starting_at_0; + now_week_day := to_calendar(now_utc, .LOCAL).day_of_week_starting_at_0; // Reset theme and clear screen. attrset(A_NORMAL); @@ -1101,7 +1101,7 @@ draw_tui :: (db: *Database, layout: *Layout) { idx := adjust_first_day_of_week[it]; x += 1; - // Apply theme. FIXME Not working (tested at 00:40 UTC+1 and showed one day earlier. + // Apply theme. if (idx == now_week_day && active_task != null) { attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); } -- cgit v1.2.3 From 2c18a63dad5a0d931f8ebeef711bfbb1d289a755 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 15:59:53 +0100 Subject: Implemented update_total_times. --- curses.jai | 12 ++++++++++++ ttt.jai | 24 +++++++++++------------- 2 files changed, 23 insertions(+), 13 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index f8715c0..718bc51 100644 --- a/curses.jai +++ b/curses.jai @@ -208,6 +208,18 @@ KEY_HOME :: 262; KEY_BACKSPACE :: 263; KEY_F0 :: 264; KEY_F :: inline (n: s32) -> s32 { return KEY_F0+n; }; +KEY_F1 :: KEY_F0 + 1; +KEY_F2 :: KEY_F0 + 2; +KEY_F3 :: KEY_F0 + 3; +KEY_F4 :: KEY_F0 + 4; +KEY_F5 :: KEY_F0 + 5; +KEY_F6 :: KEY_F0 + 6; +KEY_F7 :: KEY_F0 + 7; +KEY_F8 :: KEY_F0 + 8; +KEY_F9 :: KEY_F0 + 9; +KEY_F10 :: KEY_F0 + 10; +KEY_F11 :: KEY_F0 + 11; +KEY_F12 :: KEY_F0 + 12; KEY_DL :: 328; KEY_IL :: 329; KEY_DC :: 330; diff --git a/ttt.jai b/ttt.jai index 0955d09..b355059 100644 --- a/ttt.jai +++ b/ttt.jai @@ -563,15 +563,15 @@ update_times :: (db: *Database) { } */ } -/* + // Recalculates database totals. -void update_total_times(database_st *db) { - assert(db != NULL); +update_total_times :: (db: *Database) { + assert(db != null); - int64_t *totals = db->total_times; - memset(totals, 0, NUM_WEEK_DAYS * SIZEOF_INT64); - for (size_t idx = 0; idx < db->count; idx++) { - int64_t *times = db->tasks[idx].times; + totals: []s64 = db.total_times; + memset(totals.data, 0, NUM_WEEK_DAYS * size_of(s64)); + for db.tasks { + times : []s64 = it.times; totals[0] = add_int64(totals[0], times[0]); totals[1] = add_int64(totals[1], times[1]); totals[2] = add_int64(totals[2], times[2]); @@ -581,7 +581,7 @@ void update_total_times(database_st *db) { totals[6] = add_int64(totals[6], times[6]); } } - +/* // Resets the times of the provided task (and adjusts database totals). void reset_task_times(database_st *db, task_st *task) { assert(db != NULL); @@ -1744,13 +1744,11 @@ main :: () { trigger_autosave(); break; } - - case KEY_F(5): { + */ + case KEY_F5; update_total_times(db); trigger_autosave(); - break; - } - + /* case 't': case 'T': { if (active_task == NULL) { -- cgit v1.2.3 From f0c4c2e5b453403c367528c7425f8cc0954ab924 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 16:00:06 +0100 Subject: Draw selected/total tasks and daily totals. --- ttt.jai | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index b355059..4d1fd27 100644 --- a/ttt.jai +++ b/ttt.jai @@ -179,6 +179,12 @@ Text_Encoding :: enum u8 #specified { UTF8 :: 2; } +// TODO Provide good description. +number_size :: (number: s64, base: s64 = 10) -> s64 { + if number == 0 return 1; + return cast(s64)floor(log(cast(float64)abs(number))/log(cast(float64)abs(base))) + 1; // TODO So many casts. +} + // WIP TODO Ues compiler time code to see the auto bake being used... just for fun, once! :D truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 #no_abc { // TODO Should I use #no_abc ? assert(str.data != null); @@ -1177,46 +1183,46 @@ draw_tui :: (db: *Database, layout: *Layout) { attrset(A_NORMAL); } - return; /* + /////////////////////////////////////////////////////////////////////////// // Draw selected/total tasks. - int size = snprintf(NULL, 0, " %td/%zd ", db->selected_task + 1, db->count); - if (size <= layout->columns[L_TITLE_IDX].width) { - mvprintw(size_y - 1, 1, " %td/%zd ", db->selected_task + 1, db->count); + size := 1 + number_size(db.selected_idx + 1) + 1 + number_size(db.tasks.count) + 1; // " XXX/YYY " + if (size <= layout.columns[L_TITLE_IDX].width) { + mvprintw(size_y - 1, 1, " %td/%zd ", db.selected_idx + 1, db.tasks.count); } else { - mvprintw(size_y - 1, 1, "%td", db->selected_task + 1); + mvprintw(size_y - 1, 1, "%td", db.selected_idx + 1); } + /////////////////////////////////////////////////////////////////////////// // Draw daily totals. y = size_y - 1; - x = 0 + 1 + layout->columns[L_TITLE_IDX].width; + x = 0 + 1 + layout.columns[L_TITLE_IDX].width; total_time = 0; - for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) { - int idx = adjust_first_day_of_week[raw_idx]; - int64_t daily_total = db->total_times[idx]; - x++; + for 0..NUM_WEEK_DAYS-1 { + idx := adjust_first_day_of_week[it]; + daily_total := db.total_times[idx]; + x += 1; // Apply theme. - if (idx == now_week_day && active_task != NULL) { - attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD); + if (idx == now_week_day && active_task != null) { + attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); } else if (idx == now_week_day) { - attron(COLOR_PAIR(SELECTED_INVERTED) | A_BOLD); + attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); } - column_width = layout->columns[L_DAYS_IDX + idx].width; + column_width = layout.columns[L_DAYS_IDX + idx].width; total_time = add_int64(total_time, daily_total); - mvprintw_time(y, x, daily_total, column_width); + mvprintw_time(xx y, xx x, daily_total, xx column_width); x += column_width; // Reset theme. attrset(A_NORMAL); } - x++; - mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width); - */ + x += 1; + mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); } /* void *mem_alloc(size_t mem_size, const char *error_tag) { -- cgit v1.2.3 From d892449ee36a63ddf1bdac2fbf14f3ccaabef8fc Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 16:50:59 +0100 Subject: Correctly initialize Database struct. --- ttt.jai | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 4d1fd27..ec35973 100644 --- a/ttt.jai +++ b/ttt.jai @@ -54,8 +54,8 @@ Task :: struct { Database :: struct { modified_on : Apollo_Time; - active_idx : s64; - selected_idx : s64; + active_idx : s64 = -1; + selected_idx : s64 = -1; total_times : [NUM_WEEK_DAYS] s64; tasks : [] Task; capacity : s64; -- cgit v1.2.3 From 1c54c3afa2677c001836f84c66cdff1f381e79fe Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 16:51:38 +0100 Subject: Remove obsolete code and allow archive toggle. --- ttt.jai | 144 ++++++++++++++----------------------------------------------- unused.jai | 25 +++++++++++ 2 files changed, 58 insertions(+), 111 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index ec35973..7b49eff 100644 --- a/ttt.jai +++ b/ttt.jai @@ -328,6 +328,7 @@ get_selected_task :: inline (db: Database) -> *Task { add_task :: (db: *Database, task: Task) -> success: bool { assert(db != null, "Parameter 'db' is null."); + // TODO Remove old implementation... or use the dynamics arrays? // idx := db.tasks.count; // maybe_grow(*db.tasks); // TODO Check for errors? // db.tasks.count += 1; @@ -346,7 +347,6 @@ add_task :: (db: *Database, task: Task) -> success: bool { ifx current_capacity > (S64_MAX >> 1) then S64_MAX else current_capacity << 1; - print("expanding from % to %\n", current_capacity, new_capacity); new_tasks := realloc(db.tasks.data, new_capacity * size_of(Task), current_capacity * size_of(Task)); if (new_tasks == null) { print_error("Failed to expand database."); @@ -758,13 +758,9 @@ import_from_csv :: (db: *Database, path: string) -> bool { data: string; is_using_map := false; if size >= 1<<30 { - print("big file with % MB\n", size>>20); // TODO - map, success = map_entire_file_start(path); - data = map.data; - is_using_map = true; + assert(false, "Parsing big files not implemented yet."); } else { - print("small file with % B\n", size); // TODO data, success = read_entire_file(path); } defer if is_using_map then map_entire_file_end(*map); else free(data.data); @@ -857,7 +853,6 @@ import_from_csv :: (db: *Database, path: string) -> bool { // reset_temporary_storage(); // } } - print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); reset_temporary_storage(); return true; @@ -1245,13 +1240,7 @@ free_memory :: () { free(ar_file_path); //reset_temporary_storage(); } - /* -void exit_gracefully(int signal) { - flushinp(); - ungetch('q'); -} - void read_input_to_string_buffer_with_space(int row, int column, int style, int length, int space) { assert(length < string_buffer_size); assert(space < string_buffer_size); @@ -1482,12 +1471,6 @@ main :: () { initialize_tui(); - // TODO Remove this?! - //signal(SIGTERM, exit_gracefully); - //signal(SIGINT, exit_gracefully); - //signal(SIGQUIT, exit_gracefully); - //signal(SIGHUP, exit_gracefully); - layout := *layouts[Layouts.COMPACT]; db := *database; @@ -1766,32 +1749,30 @@ main :: () { */ case #char "\n"; #through; case #char " "; - if (db != *database || selected_task == null) break; + if (db != *database || selected_task == null) + break; // BUG The break does not work inside an if_case. Review this and all other usages. set_active_task(db, ifx (active_task == selected_task) then null else selected_task); active_task = get_active_task(db); trigger_autosave(); - - /* - case '\t': { - if (db == &database) { - if (import_from_csv(&archive, ar_file_path) == false) { - reset_database(&archive); + + case #char "\t"; + if (db == *database) { + if (import_from_csv(*archive, ar_file_path) == false) { + reset_database(*archive); print_error("Failed to load archive."); break; } - db = &archive; + db = *archive; } else { - if (export_to_csv(&archive, ar_file_path) == false) { + if (export_to_csv(*archive, ar_file_path) == false) { print_error("Failed to store archive."); break; } - reset_database(&archive); - db = &database; + reset_database(*archive); + db = *database; } - break; - } - +/* case 'a': case 'A': { if (db != &database || selected_task == NULL || selected_task == active_task) { @@ -1857,86 +1838,27 @@ main :: () { } // Save any unsaved changes. -// show_processing(); + show_processing(); error_saving := false; -// if (db == &archive) { -// if (export_to_csv(&archive, ar_file_path) == false) { -// print_error("Failed to save archive."); -// error_saving |= true; -// } -// } -// if (countdown_to_autosave > 0 || is_autosave_enabled == false) { -// if (store_database(&database, db_file_path) == false) { -// print_error("Failed to save database."); -// error_saving |= true; -// } -// } -// if (error_saving) { -// print_error("Press any key to close."); -// draw_error_window(); -// timeout(INPUT_AWAIT_INF); -// getch(); -// } + if (db == *archive) { + if (export_to_csv(archive, ar_file_path) == false) { + print_error("Failed to save archive."); + error_saving |= true; + } + } + if (countdown_to_autosave > 0 || is_autosave_enabled == false) { + if (store_database(database, db_file_path) == false) { + print_error("Failed to save database."); + error_saving |= true; + } + } + if (error_saving) { + print_error("Press any key to close."); + draw_error_window(); + timeout(INPUT_AWAIT_INF); + getch(); + } endwin(); exit(xx ifx error_saving then 1 else 0); } - -/* -main :: () { - - print("TNL %\n", TASK_NAME_LENGTH); - print("TNB %\n", TASK_NAME_BYTES); - - home, success := get_home_directory(); - print("home '%' | success '%'\n", home, success); - - print("Task Time Tracker version %\n", VERSION); - print("Copyright 2022 Daniel Martins\n"); - print("License GPL-3.0-or-later\n"); - -// TODO More binding examples here https://github.com/vrcamillo/jai-tracy -// short : s16 -// int : s32 -// long : s64 (int) -// single : float32 (float) -// double : float64 - - stdscr := initscr(); - curs_set(0); - noecho(); - box(stdscr, 0, 0); - while true { - key := getch(); - if key == #char "q" break; - mvaddstr(1, 1, "> wow alçapão <"); - } - endwin(); - -} -*/ - -// TODO DEBUG -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); -} - -// TODO DEBUG -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] - ); - } -} diff --git a/unused.jai b/unused.jai index 264fa88..a218aef 100644 --- a/unused.jai +++ b/unused.jai @@ -1,5 +1,30 @@ print(">%<", S64_MIN + delta, to_standard_error = true); + +// TODO DEBUG +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); +} +// TODO DEBUG +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] + ); + } +} + // --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // -- cgit v1.2.3 From 6d9b7b4f2d59330c3910f5cad5500ec880aef935 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 6 Apr 2023 16:54:07 +0100 Subject: Ported select_task. --- ttt.jai | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 7b49eff..ee0b344 100644 --- a/ttt.jai +++ b/ttt.jai @@ -908,7 +908,7 @@ select_task_by_delta :: (db: *Database, delta: s64) { db.selected_idx + delta; select_task_by_index(db, idx); } -/* + // Selects task. select_task :: (db: *Database, task: *Task) { assert(db != null); @@ -916,7 +916,7 @@ select_task :: (db: *Database, task: *Task) { assert(task >= db.tasks.data && task - db.tasks.data < db.tasks.count); db.selected_idx = task - db.tasks.data; } -*/ + // Set active task. // Passing task as NULL de-activates any previously active task. set_active_task :: (db: *Database, task: *Task) { @@ -1737,16 +1737,12 @@ main :: () { case KEY_F5; update_total_times(db); trigger_autosave(); - /* - case 't': - case 'T': { - if (active_task == NULL) { - break; - } + + case #char "t"; #through; + case #char "T"; + if (active_task == null) break; // BUG select_task(db, active_task); - break; - } - */ + case #char "\n"; #through; case #char " "; if (db != *database || selected_task == null) -- cgit v1.2.3 From 5a6e67d6be44e168d690d262935d5affade37fc6 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 7 Apr 2023 01:27:07 +0100 Subject: Ported reset task time feature. --- curses.jai | 2 ++ ttt.jai | 51 +++++++++++++++++++++++---------------------------- 2 files changed, 25 insertions(+), 28 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index 718bc51..6dc448d 100644 --- a/curses.jai +++ b/curses.jai @@ -102,9 +102,11 @@ noecho :: () -> s32 #foreign ncurses box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; timeout :: (delay: s32) -> void #foreign ncurses; +addch :: (ch: u32) -> s32 #foreign ncurses; mvaddch :: (y: s32, x: s32, ch: u32) -> s32 #foreign ncurses; clear :: () -> s32 #foreign ncurses; refresh :: () -> s32 #foreign ncurses; +move :: (y: s32, x: s32) -> s32 #foreign ncurses; getmaxyx :: inline (win: *WINDOW, y: *s32, x: *s32) { < s32 { return ifx win == null then ERR else win._maxx + 1; } diff --git a/ttt.jai b/ttt.jai index ee0b344..533140b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -587,24 +587,22 @@ update_total_times :: (db: *Database) { totals[6] = add_int64(totals[6], times[6]); } } -/* + // Resets the times of the provided task (and adjusts database totals). -void reset_task_times(database_st *db, task_st *task) { - assert(db != NULL); - assert(task != NULL); - assert(task >= db->tasks && task - db->tasks < db->count); +reset_task_times :: (db: *Database, task: *Task) { + assert(db != null); + assert(task != null); + assert(task >= db.tasks.data && task - db.tasks.data < db.tasks.count); // Make sure we sync before applying the changes. update_times(db); - for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { - int64_t *timer = &task->times[idx]; - int64_t *total = &db->total_times[idx]; - *total = sub_int64(*total, *timer); - *timer = 0; + for * task.times { + db.total_times[it_index] = sub_int64(db.total_times[it_index], < bool { + assert(message.data != null); - attron(style); - move(row, 1); - for (int idx = 0; idx < size_x - 2; idx++) { + attron(xx style); + move(xx row, 1); + for 0..size_x-3 { + //for (int idx = 0; idx < size_x - 2; idx++) { addch(ACS_CKBOARD); } - mvaddstr(row, 2, message); + mvaddstr(xx row, 2, message.data); attrset(A_NORMAL); - return getch() == '\n'; + return getch() == #char "\n"; } -*/ + main :: () { @@ -1572,19 +1571,15 @@ main :: () { } break; } - - case KEY_BACKSPACE: { - if (selected_task == NULL) { - break; - } + */ + case KEY_BACKSPACE; + if (selected_task == null) break; // BUG if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) { reset_task_times(db, selected_task); trigger_autosave(); } - break; - } - + /* case KEY_DC: { // Delete if (selected_task == NULL || selected_task == active_task) { break; -- cgit v1.2.3 From 3bda0a6e82af3e09336b7f715de4d5aded28b78b Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 7 Apr 2023 02:10:01 +0100 Subject: Porting delete_task - WIP. --- ttt.jai | 75 +++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 38 insertions(+), 37 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 533140b..454586d 100644 --- a/ttt.jai +++ b/ttt.jai @@ -61,6 +61,7 @@ Database :: struct { capacity : s64; } +SIZE_OF_TASK :: #run size_of(Task); // const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; // const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; // const size_t SIZEOF_TASK_ST = sizeof(task_st); @@ -322,6 +323,10 @@ get_selected_task :: inline (db: Database) -> *Task { return task; } +contains_task :: inline (db: *Database, task: *Task) -> bool { + return task >= db.tasks.data && task - db.tasks.data < db.tasks.count; +} + // Adds a task to the database. // If necessary, expands database capacity. // Returns success. @@ -347,7 +352,7 @@ add_task :: (db: *Database, task: Task) -> success: bool { ifx current_capacity > (S64_MAX >> 1) then S64_MAX else current_capacity << 1; - new_tasks := realloc(db.tasks.data, new_capacity * size_of(Task), current_capacity * size_of(Task)); + new_tasks := realloc(db.tasks.data, new_capacity * SIZE_OF_TASK, current_capacity * SIZE_OF_TASK); if (new_tasks == null) { print_error("Failed to expand database."); return false; @@ -439,54 +444,54 @@ bool duplicate_task(database_st *db, task_st *task) { return true; } - +*/ // Deletes task from database. // If possible, shrinks the database capacity. // Returns success. -bool delete_task(database_st *db, task_st *task) { - assert(db != NULL); - assert(task != NULL); - assert(task >= db->tasks && task - db->tasks < db->count); +delete_task :: (db: *Database, task: *Task) -> bool { + assert(db != null); + assert(task != null); + assert(contains_task(db, task)); // Remove task timer values from total timers. - for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { - db->total_times[idx] = sub_int64(db->total_times[idx], task->times[idx]); + for task.times { + db.total_times[it_index] = sub_int64(db.total_times[it_index], it); } // Move tasks after the index position to their new positions. - ptrdiff_t index = task - db->tasks; - memmove(task, task + 1, (db->count - index - 1) * SIZEOF_TASK_ST); - db->count--; + index := task - db.tasks.data; + memmove(task, task + 1, (db.tasks.count - index - 1) * SIZE_OF_TASK); // FIXME memmove is not implemented + db.tasks.count -= 1; // Adjust selected task. - if (db->selected_task >= db->count) { - db->selected_task--; + if (db.selected_idx >= db.tasks.count) { + db.selected_idx -= 1; } // Adjust active task. - if (db->active_task > index) { - db->active_task--; + if (db.active_idx > index) { + db.active_idx -= 1; } - else if (db->active_task == index) { - db->active_task = -1; + else if (db.active_idx == index) { + db.active_idx = -1; } // If possible, shrink database capacity. - size_t current_capacity = db->capacity; - if (db->count <= (current_capacity >> 2)) { - size_t new_capacity = current_capacity >> 1; - task_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST); - if (new_tasks == NULL && new_capacity > 0) { + current_capacity := db.capacity; + if (db.tasks.count <= (current_capacity >> 2)) { + new_capacity := current_capacity >> 1; + new_tasks := realloc(db.tasks.data, new_capacity * SIZEOF_TASK_ST); + if (new_tasks == null && new_capacity > 0) { print_error("Failed to shrink database."); return false; } - db->capacity = new_capacity; - db->tasks = new_tasks; + db.capacity = new_capacity; + db.tasks.data = new_tasks; } return true; } - +/* // Moves task to index. // Index gets clamped to [0, db->count[. void move_task_to_index(database_st *db, task_st *task, ptrdiff_t index) { @@ -592,7 +597,7 @@ update_total_times :: (db: *Database) { reset_task_times :: (db: *Database, task: *Task) { assert(db != null); assert(task != null); - assert(task >= db.tasks.data && task - db.tasks.data < db.tasks.count); + assert(contains_task(db, task)); // Make sure we sync before applying the changes. update_times(db); @@ -655,7 +660,7 @@ store_database :: (db: Database, path: string) -> success: bool { 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); + file_write(*file, db.tasks.data, SIZE_OF_TASK * db.tasks.count); return true; } @@ -693,11 +698,11 @@ load_database :: (db: *Database, path: string) -> success: bool { assert(read_success == true, "Failed to read database info from '%'.", path); // Reserve database capacity for tasks. - db.tasks.data = alloc(db.tasks.count * size_of(Task)); + db.tasks.data = alloc(db.tasks.count * SIZE_OF_TASK); db.capacity = db.tasks.count; // Read database tasks. - file_read(file, db.tasks.data, size_of(Task) * db.tasks.count); + file_read(file, db.tasks.data, SIZE_OF_TASK * db.tasks.count); // Make sure we are reading all the file. buffer: u8; @@ -1579,19 +1584,15 @@ main :: () { reset_task_times(db, selected_task); trigger_autosave(); } - /* - case KEY_DC: { // Delete - if (selected_task == NULL || selected_task == active_task) { - break; - } + + case KEY_DC; // Delete + if (selected_task == null || selected_task == active_task) break; // BUG if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) { delete_task(db, selected_task); trigger_autosave(); } - break; - } - +/* case '1': case '2': case '3': -- cgit v1.2.3 From 81ba4ca925c4e66b107bb7df737b8fdd51eee9ac Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 14 Apr 2023 09:03:25 +0100 Subject: Using resizable array to hold tasks. Implemented create new task, rename task. Fixed case break bug. --- .gitignore | 1 + curses.jai | 4 +- ttt.jai | 344 ++++++++++++++++++++++++------------------------------------- unused.jai | 3 +- 4 files changed, 140 insertions(+), 212 deletions(-) create mode 100644 .gitignore (limited to 'ttt.jai') diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30bcfa4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/curses.jai b/curses.jai index 6dc448d..737b9c2 100644 --- a/curses.jai +++ b/curses.jai @@ -82,12 +82,13 @@ ncurses :: #library "libncurses"; stdscr : *WINDOW; initscr :: () -> *WINDOW #foreign ncurses; -getch :: () -> s32 #foreign ncurses; +getch :: () -> s32 #foreign ncurses; endwin :: () -> void #foreign ncurses; cbreak :: () -> void #foreign ncurses; start_color :: () -> void #foreign ncurses; use_default_colors :: () -> void #foreign ncurses; flushinp :: () -> s32 #foreign ncurses; +echo :: () -> s32 #foreign ncurses; keypad :: (win: *WINDOW, bf: bool) -> s32 #foreign ncurses; ungetch :: (ch: s32) -> s32 #foreign ncurses; @@ -98,6 +99,7 @@ erase :: () -> s32 #foreign ncurses curs_set :: (visibility: s32) -> s32 #foreign ncurses; mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; mvprintw :: (y: s32, x: s32, fmt: *u8, args: ..Any) -> s32 #foreign ncurses; +mvgetnstr :: (y: s32, x: s32, str: *u8, n: s32) -> s32 #foreign ncurses; noecho :: () -> s32 #foreign ncurses; box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; diff --git a/ttt.jai b/ttt.jai index 454586d..ad93b3b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,7 +17,7 @@ // - release : jai ttt.jai -import_dir . -quiet -x64 -release // - debug : jai ttt.jai -import_dir . -quiet -x64 -#import "Basic"()(MEMORY_DEBUGGER=true); +#import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. #import "System"; #import "Math"; #import "POSIX"; @@ -57,8 +57,7 @@ Database :: struct { active_idx : s64 = -1; selected_idx : s64 = -1; total_times : [NUM_WEEK_DAYS] s64; - tasks : [] Task; - capacity : s64; + tasks : [..] Task; } SIZE_OF_TASK :: #run size_of(Task); @@ -327,96 +326,14 @@ contains_task :: inline (db: *Database, task: *Task) -> bool { return task >= db.tasks.data && task - db.tasks.data < db.tasks.count; } -// Adds a task to the database. +// Adds a task to the database and returns it. // If necessary, expands database capacity. -// Returns success. -add_task :: (db: *Database, task: Task) -> success: bool { +add_task :: (db: *Database, task: Task = .{}) -> task: *Task { assert(db != null, "Parameter 'db' is null."); - - // TODO Remove old implementation... or use the dynamics arrays? -// idx := db.tasks.count; -// maybe_grow(*db.tasks); // TODO Check for errors? -// db.tasks.count += 1; -// db.tasks[idx] = task; - - if (db.tasks.count == S64_MAX) { - print_error("Database reached maximum capacity."); - return false; - } - - // If necessary, expand database capacity. - current_capacity := db.capacity; - if ((db.tasks.count + 1) > current_capacity) { - new_capacity := - ifx current_capacity == 0 then 8 else - ifx current_capacity > (S64_MAX >> 1) then S64_MAX else - current_capacity << 1; - - new_tasks := realloc(db.tasks.data, new_capacity * SIZE_OF_TASK, current_capacity * SIZE_OF_TASK); - if (new_tasks == null) { - print_error("Failed to expand database."); - return false; - } - - db.tasks.data = new_tasks; - db.capacity = new_capacity; - } - - db.tasks.count += 1; - db.tasks[db.tasks.count-1] = task; - - // Adjust selected task. - if (db.selected_idx < 0) { - db.selected_idx = db.tasks.count-1; - } - - return true; + array_add(*db.tasks, task); + return *db.tasks[db.tasks.count-1]; } /* -// Creates new task stored at location given by task pointer. -// If necessary, expands database capacity. -// Returns success. -bool create_task(database_st *db, task_st **task) { - assert(db != NULL); - assert(task != NULL); - - if (db->count >= MAX_DATABASE_TASKS) { - print_error("Database reached maximum capacity."); - return false; - } - - // If necessary, expand database capacity. - size_t current_capacity = db->capacity; - if ((db->count + 1) > current_capacity) { - size_t new_capacity = - current_capacity == 0 ? 2 : - current_capacity > (MAX_DATABASE_TASKS >> 1) ? MAX_DATABASE_TASKS : - current_capacity << 1; - - task_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST); - if (new_tasks == NULL) { - print_error("Failed to expand database."); - return false; - } - - db->capacity = new_capacity; - db->tasks = new_tasks; - } - - // Prepare new task. - *task = &db->tasks[db->count]; - memset(*task, 0, SIZEOF_TASK_ST); - - db->count++; - - // Adjust selected task. - if (db->selected_task < 0) { - db->selected_task = db->count-1; - } - - return true; -} - // Duplicates the given task. Duplicated task is appended to the database. // Returns success. bool duplicate_task(database_st *db, task_st *task) { @@ -449,6 +366,7 @@ bool duplicate_task(database_st *db, task_st *task) { // If possible, shrinks the database capacity. // Returns success. delete_task :: (db: *Database, task: *Task) -> bool { + assert(db != null); assert(task != null); assert(contains_task(db, task)); @@ -460,7 +378,8 @@ delete_task :: (db: *Database, task: *Task) -> bool { // Move tasks after the index position to their new positions. index := task - db.tasks.data; - memmove(task, task + 1, (db.tasks.count - index - 1) * SIZE_OF_TASK); // FIXME memmove is not implemented + for index..db.tasks.count-2 + db.tasks[it] = db.tasks[it+1]; db.tasks.count -= 1; // Adjust selected task. @@ -477,16 +396,31 @@ delete_task :: (db: *Database, task: *Task) -> bool { } // If possible, shrink database capacity. - current_capacity := db.capacity; - if (db.tasks.count <= (current_capacity >> 2)) { - new_capacity := current_capacity >> 1; - new_tasks := realloc(db.tasks.data, new_capacity * SIZEOF_TASK_ST); - if (new_tasks == null && new_capacity > 0) { - print_error("Failed to shrink database."); - return false; + // TODO Do we really want to make this fuss? + current_capacity := db.tasks.allocated; + if (db.tasks.count < (current_capacity >> 2)) { + new_capacity := 1 << (get_msb(db.tasks.count) + 2); + my_array_reserve_nonpoly(xx *db.tasks, new_capacity, SIZE_OF_TASK); + } + + get_msb :: (value: s64) -> msb: s64, found: bool { + result: s64 = ---; + #asm { + mov val: gpr, value; + bsr result, val; } - db.capacity = new_capacity; - db.tasks.data = new_tasks; + return result * xx cast(bool)value, xx value; // If value is zero: return `0, false`. + } + + my_array_reserve_nonpoly :: (array: *[..] *void, desired_items: s64, size: s64) -> success: bool { + if !array.allocator.proc remember_allocators(array); + + new_array_data := realloc(array.data, desired_items * size, array.allocated * size, array.allocator); + if new_array_data == null return false; + + array.data = new_array_data; + array.allocated = desired_items; + return true; } return true; @@ -698,11 +632,13 @@ load_database :: (db: *Database, path: string) -> success: bool { assert(read_success == true, "Failed to read database info from '%'.", path); // Reserve database capacity for tasks. - db.tasks.data = alloc(db.tasks.count * SIZE_OF_TASK); - db.capacity = db.tasks.count; + 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 * db.tasks.count); + 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; @@ -744,17 +680,16 @@ export_to_csv :: (db: Database, path: string) -> success: bool { // Imports CSV file into database. // Returns success. import_from_csv :: (db: *Database, path: string) -> bool { - - // TODO WIP - + // TODO Review code. assert(db != null, "Parameter 'db' is null."); assert(xx path, "Parameter 'path' is empty."); error_code: s64; - // Check file size - file_info: stat_t; - error_code = sys_stat(path, *file_info); // TODO Check for error. - size := file_info.st_size; + // Check file size TODO Read based on file size + //file_info: stat_t; + //error_code = sys_stat(path, *file_info); // TODO Check for error. + //size := file_info.st_size; + size := 0; success: bool; map: Map_File_Info; @@ -828,35 +763,37 @@ import_from_csv :: (db: *Database, path: string) -> bool { return "", false; } - // Parse CSV lines. - line := csv; - while (true) { - line, success := consume_next_line(*csv); -// line, success := next_line(*csv); - if success == false break; - - task: Task; - csv_values := split(line, ","); // TODO Temporary memory... if file is too big this may break. - - // Import task name. - name_length := min(task.name.count, csv_values[0].count); - memcpy(task.name.data, csv_values[0].data, name_length); - truncate_string(xx task.name, name_length); - - advance(*csv_values); - for csv_values { - parsed_value := string_to_int(it); - task.times[it_index] = parsed_value; - db.total_times[it_index] = add_int64(db.total_times[it_index], parsed_value); + { // Parse CSV lines. + auto_release_temp(); // TODO Needs to be tested. + line := csv; + while (true) { + line, success := consume_next_line(*csv); + // line, success := next_line(*csv); + if success == false break; + + task: Task; + csv_values := split(line, ","); // TODO Temporary memory... if file is too big this may break. + + // Import task name. + name_length := min(task.name.count, csv_values[0].count); + memcpy(task.name.data, csv_values[0].data, name_length); + truncate_string(xx task.name, name_length); + + advance(*csv_values); + for csv_values { + parsed_value := string_to_int(it); + task.times[it_index] = parsed_value; + db.total_times[it_index] = add_int64(db.total_times[it_index], parsed_value); + } + + add_task(db, *task); + // TODO Check this old code and remove it if not necessary. + // if context.temporary_storage.total_bytes_occupied > (100<<20) { + // print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); + // reset_temporary_storage(); + // } } - - add_task(db, *task); -// if context.temporary_storage.total_bytes_occupied > (100<<20) { -// print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); -// reset_temporary_storage(); -// } } - reset_temporary_storage(); return true; } @@ -928,13 +865,11 @@ set_active_task :: (db: *Database, task: *Task) { update_times(db); db.active_idx = ifx (task == null) then -1 else task - db.tasks.data; } -/* + // Returns true when database is full. -bool is_database_full(database_st *db) { - assert(db != NULL); - return db->count >= MAX_DATABASE_TASKS; +is_database_full :: inline (db: Database) -> bool { + return db.tasks.count >= MAX_DATABASE_TASKS; } -*/ INPUT_TIMEOUT_MS :: 1000; INPUT_AWAIT_INF :: -1; @@ -1243,23 +1178,22 @@ free_memory :: () { free(ar_file_path); //reset_temporary_storage(); } -/* -void read_input_to_string_buffer_with_space(int row, int column, int style, int length, int space) { - assert(length < string_buffer_size); - assert(space < string_buffer_size); - + +read_input_with_space :: (row: int, column: int, style: s32, length: int, space: int) -> string { + str := talloc_string(length); + memset(str.data, 0, str.count); attron(style | A_UNDERLINE); - mvprintw(row, column, "%*s", space, ""); + mvprintw(xx row, xx column, "%*s", space, ""); echo(); curs_set(1); - memset(string_buffer, 0, string_buffer_size); - mvgetnstr(row, column, string_buffer, length); - truncate_string_utf8(string_buffer, length); + mvgetnstr(xx row, xx column, str.data, xx length); + truncate_string(str, length); noecho(); curs_set(0); attrset(A_NORMAL); + return str; } - +/* void read_input_to_string_buffer(int row, int column, int style, int length) { read_input_to_string_buffer_with_space(row, column, style, length, length); } @@ -1306,20 +1240,23 @@ read_enter_confirmation :: (row: int, style: int, message: string) -> bool { return getch() == #char "\n"; } - main :: () { - defer report_memory_leaks(); // TODO DEBUG + defer report_memory_leaks(); // TODO Remove after final debug sessions. defer free_memory(); { // Initialize app directory. + auto_release_temp(); 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. TODO LEAK + home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. + + + if success_path == false { print_error("Failed to find home directory '%'.", home_dir); exit(1); @@ -1331,9 +1268,9 @@ main :: () { make_directory_if_it_does_not_exist(app_directory, recursive = true); } - + { // Initialize database and archive files if needed. - + auto_release_temp(); if (file_exists(db_file_path) == false) { if (store_database(database, db_file_path) == false) { print_error("Failed to initialize database."); @@ -1475,14 +1412,28 @@ main :: () { initialize_tui(); - layout := *layouts[Layouts.COMPACT]; db := *database; + layout := *layouts[Layouts.COMPACT]; flushinp(); ungetch(KEY_RESIZE); while (true) { + + if (is_terminal_too_small) { + INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; + mvaddstr(size_y / 2, (size_x - xx INVALID_WINDOW_MESSAGE.count) / 2, INVALID_WINDOW_MESSAGE); + } + else { + draw_tui(db, layout); + draw_error_window(); + } + + reset_temporary_storage(); + timeout(INPUT_TIMEOUT_MS); key := getch(); if key == #char "q" || key == #char "Q" break; + update_times(*database); + timeout(INPUT_AWAIT_INF); active_task := get_active_task(db); selected_task := get_selected_task(db); @@ -1494,9 +1445,6 @@ main :: () { else ifx (db.selected_idx < 0) then 1 else (db.selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS; - timeout(INPUT_AWAIT_INF); - update_times(*database); - if key == { // When getch() times out. @@ -1530,24 +1478,20 @@ main :: () { //} update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; -/* - case 'n': - case 'N':{ - if (is_database_full(db)) { + + case #char "n"; #through; + case #char "N"; + if is_database_full(db) { read_enter_confirmation(selected_task_row, error_style, " Unable to create entry: database is full. "); - break; + continue; } // Create new task. - task_st *new_task; - if (create_task(db, &new_task) == false) { - break; - } - - // Set new task name. - time_t now_utc = time(NULL); - struct tm *now_local = localtime(&now_utc); - strftime(new_task->name, TASK_NAME_BYTES, "%Y-%m-%d %H:%M:%S", now_local); + now_utc := current_time_consensus(); + now_local := to_calendar(now_utc, .LOCAL); + name := calendar_to_iso_string(now_local); + new_task := add_task(db); + memcpy(new_task.name.data, name.data, min(Task.name.count, name.count)); // Select new task. select_task(db, new_task); @@ -1557,28 +1501,21 @@ main :: () { // Force rename action. flushinp(); ungetch(KEY_F(2)); - break; - } - - case KEY_F(2): { - if (selected_task == NULL) { - break; - } + + case KEY_F2; + if (selected_task == null) continue; - read_input_to_string_buffer_with_space(selected_task_row, 1, action_style, TASK_NAME_LENGTH, size_x - 2); - if (is_empty_string(string_buffer) == false) { - replace_char(string_buffer, '\t', ' '); - replace_char(string_buffer, '\v', ' '); - replace_char(string_buffer, '\f', ' '); - replace_char(string_buffer, '\r', ' '); - memcpy(selected_task->name, string_buffer, TASK_NAME_BYTES); + // Change task name. + // TODO remove TASK_NAME_LENGTH + input := read_input_with_space(selected_task_row, 1, action_style, TASK_NAME_LENGTH, size_x - 2); + if is_empty_string(input) == false { + replace_chars(input, "\t\x0B\x0C\r", #char " "); + memcpy(selected_task.name.data, input.data, min(Task.name.count, input.count)); trigger_autosave(); } - break; - } - */ + case KEY_BACKSPACE; - if (selected_task == null) break; // BUG + if (selected_task == null) continue; if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) { reset_task_times(db, selected_task); @@ -1586,7 +1523,7 @@ main :: () { } case KEY_DC; // Delete - if (selected_task == null || selected_task == active_task) break; // BUG + if (selected_task == null || selected_task == active_task) continue; if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) { delete_task(db, selected_task); @@ -1736,13 +1673,12 @@ main :: () { case #char "t"; #through; case #char "T"; - if (active_task == null) break; // BUG + if (active_task == null) continue; select_task(db, active_task); case #char "\n"; #through; case #char " "; - if (db != *database || selected_task == null) - break; // BUG The break does not work inside an if_case. Review this and all other usages. + if (db != *database || selected_task == null) continue; set_active_task(db, ifx (active_task == selected_task) then null else selected_task); active_task = get_active_task(db); trigger_autosave(); @@ -1752,14 +1688,14 @@ main :: () { if (import_from_csv(*archive, ar_file_path) == false) { reset_database(*archive); print_error("Failed to load archive."); - break; + continue; } db = *archive; } else { if (export_to_csv(*archive, ar_file_path) == false) { print_error("Failed to store archive."); - break; + continue; } reset_database(*archive); db = *database; @@ -1815,18 +1751,6 @@ main :: () { case KEY_NPAGE; select_task_by_delta(db, layout_tasks_rows); } - - if (is_terminal_too_small) { - INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; - mvaddstr(size_y / 2, (size_x - xx INVALID_WINDOW_MESSAGE.count) / 2, INVALID_WINDOW_MESSAGE); - } - else { - draw_tui(db, layout); - draw_error_window(); - } - - timeout(INPUT_TIMEOUT_MS); - } // Save any unsaved changes. diff --git a/unused.jai b/unused.jai index a218aef..97d9f88 100644 --- a/unused.jai +++ b/unused.jai @@ -1,4 +1,5 @@ - print(">%<", S64_MIN + delta, to_standard_error = true); +auto_release_temp(); // TODO Needs to be tested. +print(">%<", S64_MIN + delta, to_standard_error = true); // TODO DEBUG print_owner_allocator :: (tag: string, memory: *void) { -- cgit v1.2.3 From 03616634e7b034891725864fe88e68b7e760b8a2 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 14 Apr 2023 18:46:46 +0100 Subject: Implemented most of missing features. --- curses.jai | 28 +++- ttt.jai | 466 ++++++++++++++++++++++++------------------------------------- 2 files changed, 208 insertions(+), 286 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index 737b9c2..317d09b 100644 --- a/curses.jai +++ b/curses.jai @@ -97,11 +97,15 @@ attrset :: (attrs: s32) -> s32 #foreign ncurses attron :: (attrs: s32) -> s32 #foreign ncurses; erase :: () -> s32 #foreign ncurses; curs_set :: (visibility: s32) -> s32 #foreign ncurses; +addstr :: (str: *u8) -> s32 #foreign ncurses; mvaddstr :: (y: s32, x: s32, str: *u8) -> s32 #foreign ncurses; -mvprintw :: (y: s32, x: s32, fmt: *u8, args: ..Any) -> s32 #foreign ncurses; -mvgetnstr :: (y: s32, x: s32, str: *u8, n: s32) -> s32 #foreign ncurses; +mvprintw :: (y: s32, x: s32, fmt: *u8, + args: ..Any) -> s32 #foreign ncurses; +mvgetnstr :: (y: s32, x: s32, str: *u8, + n: s32) -> s32 #foreign ncurses; noecho :: () -> s32 #foreign ncurses; -box :: (win: *WINDOW, verch: u8, horch: u8) -> s32 #foreign ncurses; +box :: (win: *WINDOW, verch: u8, + horch: u8) -> s32 #foreign ncurses; init_pair :: (pair: s16, f: s16, b: s16) -> s32 #foreign ncurses; timeout :: (delay: s32) -> void #foreign ncurses; addch :: (ch: u32) -> s32 #foreign ncurses; @@ -109,6 +113,24 @@ mvaddch :: (y: s32, x: s32, ch: u32) -> s32 #foreign ncurses clear :: () -> s32 #foreign ncurses; refresh :: () -> s32 #foreign ncurses; move :: (y: s32, x: s32) -> s32 #foreign ncurses; +isendwin :: () -> bool #foreign ncurses; +delwin :: (win: *WINDOW) -> s32 #foreign ncurses; +newwin :: (nlines: s32, ncols: s32, begin_y: s32, + begin_x: s32) -> *WINDOW #foreign ncurses; +wattron :: (win: *WINDOW, attrs: s32) -> s32 #foreign ncurses; +wborder :: (win: *WINDOW, ls: u32, rs: u32, + ts: u32, bs: u32, tl: u32, + tr: u32, bl: u32, br: u32) -> s32 #foreign ncurses; +mvwin :: (win: *WINDOW, y: s32, x: s32) -> s32 #foreign ncurses; +touchwin :: (win: *WINDOW) -> s32 #foreign ncurses; +wrefresh :: (win: *WINDOW) -> s32 #foreign ncurses; +mvwprintw :: (win: *WINDOW, y: s32, x: s32, + fmt: *u8, args: ..Any) -> s32 #foreign ncurses; +wmove :: (win: *WINDOW, y: s32, x: s32) -> s32 #foreign ncurses; +waddch :: (win: *WINDOW, ch: u32) -> s32 #foreign ncurses; +vw_printw :: (win: *WINDOW, fmt: *u8, + varglist: ..Any) -> s32 #foreign ncurses; + getmaxyx :: inline (win: *WINDOW, y: *s32, x: *s32) { < s32 { return ifx win == null then ERR else win._maxx + 1; } diff --git a/ttt.jai b/ttt.jai index ad93b3b..6279f27 100644 --- a/ttt.jai +++ b/ttt.jai @@ -29,10 +29,9 @@ VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2023"; -TASK_NAME_LENGTH :: 57; -TASK_NAME_BYTES :: #run TASK_NAME_LENGTH+1; // TODO Get rid of this! FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). -NUM_WEEK_DAYS :: 7; // Just to be more clear about what we're looping about. +NUM_WEEK_DAYS :: 7; // TODO This has to go - Just to be more clear about what we're looping about. +NAME_SIZE :: 72; // TODO Use this instead of Task.name.count ? APP_FOLDER_NAME :: ".task_time_tracker_v2"; // TODO Using _v2 to avoid erasing my work data. DB_FILE_NAME :: "database.bin"; @@ -48,8 +47,6 @@ MAX_DATABASE_TASKS :: S64_MAX; Task :: struct { times : [NUM_WEEK_DAYS] s64; name : [72] u8; - //name_data : [70] u8; - //name.data = cast(*~s8 u8) (0x80 ^ 0x01); // *name_data[0]; } Database :: struct { @@ -102,57 +99,54 @@ error_window : *WINDOW = null; error_time_limit := Apollo_Time.{0, 0}; print_error :: (format :string, args : .. Any) { - // NOTE Implement me please. -// if stdscr == null || isendwin() == true { // Not in ncurses mode? + if stdscr == null || isendwin() == true { // Not in ncurses mode? print(format, ..args); print("\n"); -// } -// else { -// int w_size_x = size_x > 120 ? 120 : size_x - 2; -// int w_size_y = 4; -// if (error_window == NULL) { -// error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); -// wattron(error_window, COLOR_PAIR(ERROR)); -// wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); -// mvwprintw(error_window, 0, 1, " Error "); -// wmove(error_window, 1, 0); -// } -// else { -// waddch(error_window, ' '); -// } -// vw_printw(error_window, format, args); -// error_time_limit = time(NULL) + 5; // NOTE Instead of time use the current_time_monotonic() -// } + } + else { + CHAR_SPACE :: #char " "; + w_size_x: s32 = ifx size_x > 120 then 120 else size_x - 2; + w_size_y: s32 = 4; + if (error_window == null) { + error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); + wattron(error_window, COLOR_PAIR(xx Styles.ERROR)); + wborder(error_window, CHAR_SPACE, CHAR_SPACE, 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); + mvwprintw(error_window, 0, 1, " Error "); + wmove(error_window, 1, 0); + } + else { + waddch(error_window, CHAR_SPACE); + } + vw_printw(error_window, to_c_string(format), args); + error_time_limit = current_time_monotonic() + seconds_to_apollo(5); + } } draw_error_window :: () { - /* NOTE Implement me please. - if (error_window == NULL) { - return; - } + if (error_window == null) return; // Hide error window after time-limit or if terminal is shrank. - int w_size_x, w_size_y; - getmaxyx(error_window, w_size_y, w_size_x); - if (time(NULL) >= error_time_limit + w_size_x: s32; + w_size_y: s32; + getmaxyx(error_window, *w_size_y, *w_size_x); + if (current_time_monotonic() >= error_time_limit || size_x - w_size_x < 2 || size_y - w_size_y < 2 ) { delwin(error_window); - error_window = NULL; + error_window = null; return; } // Adjust error window position. - int pos_x = (size_x - w_size_x) / 2; - int pos_y = (size_y - w_size_y) / 2; + pos_x := (size_x - w_size_x) / 2; + pos_y := (size_y - w_size_y) / 2; mvwin(error_window, pos_y, pos_x); // Avoid being overwritten by main window content. refresh(); touchwin(error_window); wrefresh(error_window); - */ } trigger_autosave :: () { @@ -240,12 +234,6 @@ is_empty_string :: (str: string) -> bool { return true; } -replace_char :: (str: string, find: u8, replace: u8) -> string { - assert(false, "Use modules/String/module.jai:replace_chars"); - // TODO Use modules/String/module.jai:replace_chars - return ""; -} - // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { @@ -306,23 +294,16 @@ sub_int64 :: (x :s64, y :s64) -> s64 { // Returns active task or NULL if none applies. get_active_task :: inline (db: Database) -> *Task { - task: *Task = null; - if (db.active_idx >= 0) { - task = *db.tasks[db.active_idx]; - } - return 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 { - task: *Task = null; - if (db.selected_idx >= 0) { - task = *db.tasks[db.selected_idx]; - } - return task; + return ifx db.selected_idx >= 0 then *db.tasks[db.selected_idx] else null; } -contains_task :: inline (db: *Database, task: *Task) -> bool { +// TODO Add description. +contains_task :: inline (db: Database, task: *Task) -> bool { return task >= db.tasks.data && task - db.tasks.data < db.tasks.count; } @@ -331,37 +312,14 @@ contains_task :: inline (db: *Database, task: *Task) -> bool { add_task :: (db: *Database, task: Task = .{}) -> task: *Task { assert(db != null, "Parameter 'db' is null."); array_add(*db.tasks, task); - return *db.tasks[db.tasks.count-1]; -} -/* -// Duplicates the given task. Duplicated task is appended to the database. -// Returns success. -bool duplicate_task(database_st *db, task_st *task) { - assert(db != NULL); - assert(task != NULL); - - // Create new task and keep task_idx (relative pointer) of original task). - ptrdiff_t task_idx = task - db->tasks; - task_st *new_task; - if (create_task(db, &new_task) == false) { - return false; - } - - // If original task belonged to database, fix its pointer. - if (0 <= task_idx && task_idx < db->count - 1) { // Compensate '- 1' for the new task. - task = db->tasks + task_idx; - } - - memcpy(new_task, task, SIZEOF_TASK_ST); - // Add task time values to total times. - for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { - db->total_times[idx] = add_int64(db->total_times[idx], new_task->times[idx]); + for * db.total_times { + < bool { return true; } -/* + // Moves task to index. // Index gets clamped to [0, db->count[. -void move_task_to_index(database_st *db, task_st *task, ptrdiff_t index) { - assert(db != NULL); - assert(task != NULL); - assert(task >= db->tasks && task - db->tasks < db->count); - - ptrdiff_t target_index = index < 0 ? 0 - : index >= db->count ? db->count - 1 - : index; +move_task_to_index :: (db: *Database, task: *Task, index: s64) { + assert(db != null, "Parameter 'db' is null."); + assert(task != null, "Parameter 'task' is null."); + assert(contains_task(db, task), "Database does not contain task."); - task_st *target_task = db->tasks + target_index; + target_index := clamp(index, 0, db.tasks.count-1); + target_task := *db.tasks[target_index]; - if (target_task == task) { - return; - } + if (target_task == task) return; // Move task to new location. - task_st temp_task; - memcpy(&temp_task, task, SIZEOF_TASK_ST); - if (target_task > task) { - memmove(task, task + 1, (target_task - task) * SIZEOF_TASK_ST); + temp_task := < task { + //memmove(task, task + 1, (target_task - task) * SIZEOF_TASK_ST); TODO Maybe simplify this moves. + offset := task - db.tasks.data; + size := target_task - task; + for 0..size-1 + db.tasks[offset + it] = db.tasks[offset + it + 1]; } else { - memmove(target_task + 1, target_task, (task - target_task) * SIZEOF_TASK_ST); + //memmove(target_task + 1, target_task, (task - target_task) * SIZEOF_TASK_ST); TODO Maybe simplify this moves. + offset := target_task - db.tasks.data; + size := task - target_task; + for < size-1..0 + db.tasks[offset + it + 1] = db.tasks[offset + it]; } - memcpy(target_task, &temp_task, SIZEOF_TASK_ST); + <tasks; - if (db->active_task == source_index) { - db->active_task = target_index; + source_index := task - db.tasks.data; + if (db.active_idx == source_index) { + db.active_idx = target_index; } - else if (source_index < db->active_task && db->active_task <= target_index) { - db->active_task--; + else if (source_index < db.active_idx && db.active_idx <= target_index) { + db.active_idx -= 1; } - else if (target_index <= db->active_task && db->active_task < source_index) { - db->active_task++; + else if (target_index <= db.active_idx && db.active_idx < source_index) { + db.active_idx += 1; } - db->selected_task = target_index; + db.selected_idx = target_index; } -*/ + // Updates the times on the active task (and adjusts database totals). update_times :: (db: *Database) { assert(db != null); - return; - /* - // Get current UTC time. - time_t stop_time = time(NULL); - // Get last modified on UTC time. - time_t start_time = db->modified_on; + if db.active_idx < 0 return; - // Keep track of this update. - db->modified_on = stop_time; - - if (db->active_task < 0) { - return; - } + // Get time frame in UTC. + start_time := db.modified_on; + stop_time := current_time_consensus(); - task_st *active_task = db->tasks + db->active_task; - uint8_t start_week_day; + active_task := *db.tasks[db.active_idx]; + start_week_day: s8; while (start_time < stop_time) { - start_week_day = localtime(&start_time)->tm_wday; + start_week_day = to_calendar(start_time, .LOCAL).day_of_week_starting_at_0; // Get next day in local time. - struct tm *start_of_day_tm = localtime(&start_time); - start_of_day_tm->tm_sec = 0; - start_of_day_tm->tm_min = 0; - start_of_day_tm->tm_hour = 0; - time_t start_of_day = mktime(start_of_day_tm); - time_t next_day = start_of_day + SECONDS_IN_DAY; - time_t next_start = next_day < stop_time ? next_day : stop_time; - time_t elapsed_time = next_start - start_time; - active_task->times[start_week_day] += elapsed_time; - db->total_times[start_week_day] += elapsed_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; } - */ + + // Keep track of this update. + db.modified_on = stop_time; } // Recalculates database totals. @@ -797,37 +753,35 @@ import_from_csv :: (db: *Database, path: string) -> bool { return true; } -/* + // Appends task to the end of the CSV file. // Returns success. -bool append_to_csv(task_st *task, const char *path) { - assert(task != NULL); - assert(path != NULL); +append_to_csv :: (task: Task, path: string) -> success: bool { + assert(xx path, "Parameter 'path' is empty."); - FILE *file = fopen(path, "a+"); - if (file == NULL) { - print_error("Failed to open file '%s' while appending to CSV: %s.", path, strerror(errno)); + file, file_success := file_open(path, true, true); + defer file_close(*file); + if file_success == false { + //print_error("Failed to open file '%s' while appending to CSV: %s.", path, strerror(errno)); // TODO Show internal error or something return false; } - char last_char; - fseek(file, -1, SEEK_END); - fread(&last_char, SIZEOF_CHAR, 1, file); - if (last_char != '\n') { - fprintf(file, "\n"); + 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"); } - char name[TASK_NAME_BYTES]; - memcpy(name, task->name, TASK_NAME_BYTES); - replace_char(name, ',', ' '); - fprintf(file, "%s,%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 "\n", - name, task->times[0], task->times[1], task->times[2], task->times[3], task->times[4], task->times[5], task->times[6] - ); + task_name := copy_temporary_string(xx task.name); // TODO Cleanup this temp mess. + 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]); + file_write(*file, csv_line); - fclose(file); return true; } -*/ + // Selects task by index. // Index gets clamped to [0, db->count[. select_task_by_index :: (db: *Database, index: s64) { @@ -1157,33 +1111,22 @@ draw_tui :: (db: *Database, layout: *Layout) { x += 1; mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); } -/* -void *mem_alloc(size_t mem_size, const char *error_tag) { - void *mem_pointer = malloc(mem_size); - if (mem_pointer == NULL && mem_size > 0) { - print_error("Failed to allocate memory (%s): %s.", (error_tag == NULL ? "undefined" : error_tag), strerror(errno)); - exit(EXIT_FAILURE); - } - return mem_pointer; -} -*/ free_memory :: () { reset_database(*database); reset_database(*archive); - //free(string_buffer); string_buffer = NULL; free(app_directory); free(db_file_path); free(ar_file_path); //reset_temporary_storage(); } -read_input_with_space :: (row: int, column: int, style: s32, length: int, space: int) -> string { +read_input_string_padded :: (row: int, column: int, style: s32, length: int, padding: int) -> string { str := talloc_string(length); memset(str.data, 0, str.count); attron(style | A_UNDERLINE); - mvprintw(xx row, xx column, "%*s", space, ""); + mvprintw(xx row, xx column, "%*s", padding, ""); echo(); curs_set(1); mvgetnstr(xx row, xx column, str.data, xx length); @@ -1193,37 +1136,28 @@ read_input_with_space :: (row: int, column: int, style: s32, length: int, space: attrset(A_NORMAL); return str; } -/* -void read_input_to_string_buffer(int row, int column, int style, int length) { - read_input_to_string_buffer_with_space(row, column, style, length, length); + +read_input_string :: (row: int, column: int, style: s32, length: int) -> string { + return read_input_string_padded(row, column, style, length, length); } // Returns success. -bool read_input_to_int(int row, int style, const char *message, intmax_t *result) { - assert(message != NULL); - assert(result != NULL); - - attron(style); - move(row, 1); +read_input_int :: (row: int, style: s32, message: string) -> value: int, success: bool { + attron(xx style); + move(xx row, 1); addch(ACS_CKBOARD); - addstr(message); + addstr(message.data); // TODO Convert to C type attrset(A_NORMAL); // Get line number. - int input_pos_x = getcurx(stdscr); - int input_width = size_x - input_pos_x - 1; - read_input_to_string_buffer(row, input_pos_x, style, input_width); + input_pos_x := getcurx(stdscr); + input_width := size_x - input_pos_x - 1; + str := read_input_string(row, input_pos_x, style, input_width); - char *parser; - errno = 0; - *result = strtoimax(string_buffer, &parser, 10); - - bool success = (errno == 0 || errno == ERANGE) // No error OR value was clamped to limits (acceptable). - && parser != string_buffer; // If no digits are found, parser will return the address of the input string. - - return success; + value, success := parse_int(*str); + return value, success; } -*/ + // Retuns true if user presses enter, false otherwise. read_enter_confirmation :: (row: int, style: int, message: string) -> bool { assert(message.data != null); @@ -1465,17 +1399,6 @@ main :: () { clear(); getmaxyx(stdscr, *size_y, *size_x); is_terminal_too_small = size_x < 60 || size_y < 3; - new_size := 2047 | TASK_NAME_BYTES | (size_x + 1); - //if (string_buffer_size < new_size) { - //string_buffer_size = new_size; - //string_buffer = realloc(string_buffer, string_buffer_size); - //if (string_buffer == NULL && string_buffer_size > 0) { - //print_error("Failed to allocate memory for string buffer: %s.", strerror(errno)); - //flushinp(); - //ungetch(#char "q"); - //break; - //} - //} update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; @@ -1506,8 +1429,7 @@ main :: () { if (selected_task == null) continue; // Change task name. - // TODO remove TASK_NAME_LENGTH - input := read_input_with_space(selected_task_row, 1, action_style, TASK_NAME_LENGTH, size_x - 2); + input := read_input_string_padded(selected_task_row, 1, action_style, Task.name.count, size_x - 2); if is_empty_string(input) == false { replace_chars(input, "\t\x0B\x0C\r", #char " "); memcpy(selected_task.name.data, input.data, min(Task.name.count, input.count)); @@ -1615,58 +1537,41 @@ main :: () { trigger_autosave(); break; } - - case 'm': - case 'M': { - if (selected_task == NULL) { - break; - } +*/ + case #char "m"; #through; + case #char "M"; + if selected_task == null continue; - intmax_t value; - if (read_input_to_int(selected_task_row, action_style, " Move to: ", &value) == false) { - break; - } + value, success := read_input_int(selected_task_row, action_style, " Move to: "); + if success == false continue; - ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1; + target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1; move_task_to_index(db, selected_task, target_index); trigger_autosave(); - break; - } - - case 'g': - case 'G': { - if (selected_task == NULL) { - break; - } + + case #char "g"; #through; + case #char "G"; + if selected_task == null continue; - intmax_t value; - if (read_input_to_int(selected_task_row, action_style, " Go to: ", &value) == false) { - break; - } + value, success := read_input_int(selected_task_row, action_style, " Go to: "); + if success == false continue; - ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1; + target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1; select_task_by_index(db, target_index); - break; - } - case 'd': - case 'D':{ - if (selected_task == NULL) { - break; - } + case #char "d"; #through; + case #char "D"; + if selected_task == null continue; - if (is_database_full(db)) { + if is_database_full(db) { read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate entry: database is full. "); - break; + continue; } - if (duplicate_task(db, selected_task) == false) { - break; - } + if (add_task(db, selected_task) == null) continue; // TODO Show error? + trigger_autosave(); - break; - } - */ + case KEY_F5; update_total_times(db); trigger_autosave(); @@ -1700,39 +1605,34 @@ main :: () { reset_database(*archive); db = *database; } -/* - case 'a': - case 'A': { - if (db != &database || selected_task == NULL || selected_task == active_task) { - break; - } + + 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) { print_error("Failed to archive entry."); - break; + continue; } - delete_task(&database, selected_task); + delete_task(*database, selected_task); trigger_autosave(); - break; - } - - case 'r': - case 'R': { - if (db != &archive || selected_task == NULL) { - break; - } - if (is_database_full(&database)) { + + case #char "r"; #through; + case #char "R"; + if (db != *archive || selected_task == null) continue; + + if is_database_full(*database) { read_enter_confirmation(selected_task_row, error_style, " Unable to restore entry: database is full. "); - break; + continue; } - if (duplicate_task(&database, selected_task) == false) { + + if (add_task(*database, selected_task) == null) { print_error("Failed to restore entry."); - break; + continue; } - delete_task(&archive, selected_task); + delete_task(*archive, selected_task); trigger_autosave(); - break; - } - */ + case KEY_HOME; select_task_by_index(db, 0); @@ -1754,26 +1654,26 @@ main :: () { } // Save any unsaved changes. - show_processing(); + show_processing(); error_saving := false; - if (db == *archive) { - if (export_to_csv(archive, ar_file_path) == false) { - print_error("Failed to save archive."); - error_saving |= true; - } - } - if (countdown_to_autosave > 0 || is_autosave_enabled == false) { - if (store_database(database, db_file_path) == false) { - print_error("Failed to save database."); - error_saving |= true; - } - } - if (error_saving) { - print_error("Press any key to close."); - draw_error_window(); - timeout(INPUT_AWAIT_INF); - getch(); - } + if (db == *archive) { + if (export_to_csv(archive, ar_file_path) == false) { + print_error("Failed to save archive."); + error_saving |= true; + } + } + if (countdown_to_autosave > 0 || is_autosave_enabled == false) { + if (store_database(database, db_file_path) == false) { + print_error("Failed to save database."); + error_saving |= true; + } + } + if (error_saving) { + print_error("Press any key to close."); + draw_error_window(); + timeout(INPUT_AWAIT_INF); + getch(); + } endwin(); exit(xx ifx error_saving then 1 else 0); -- cgit v1.2.3 From 2a893df3b65bfd01e666c0df0498dba1694ebebe Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 15 Apr 2023 17:27:01 +0100 Subject: Fix reset_database. --- ttt.jai | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 6279f27..08b2ea4 100644 --- a/ttt.jai +++ b/ttt.jai @@ -530,8 +530,8 @@ void add_task_time(database_st *db, task_st *task, int day, int64_t time) { // Resets database to the initial state and deallocates all memory taken by tasks. reset_database :: (db: *Database) { - assert(db != null); - free(db.tasks.data); + assert(db != null, "Parameter 'db' is null."); + array_reset(*db.tasks); < Date: Sun, 16 Apr 2023 17:34:26 +0100 Subject: Ported all features to jai. --- ttt.jai | 248 +++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 120 insertions(+), 128 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 08b2ea4..bc81901 100644 --- a/ttt.jai +++ b/ttt.jai @@ -36,8 +36,12 @@ NAME_SIZE :: 72; // TODO Use this instead of Task.name.count ? APP_FOLDER_NAME :: ".task_time_tracker_v2"; // TODO Using _v2 to avoid erasing my work data. 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_NOT_CONTAIN :: "'%' does not contain '%'."; + SECONDS_IN_MINUTE :: cast(s64)60; SECONDS_IN_HOUR :: cast(s64)60*SECONDS_IN_MINUTE; SECONDS_IN_DAY :: cast(s64)24*SECONDS_IN_HOUR; @@ -181,8 +185,8 @@ number_size :: (number: s64, base: s64 = 10) -> s64 { // WIP TODO Ues compiler time code to see the auto bake being used... just for fun, once! :D truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 #no_abc { // TODO Should I use #no_abc ? - assert(str.data != null); - assert(str.count >= length); + assert(str.data != null, ASSERT_NOT_NULL, "str"); + assert(str.count >= length, "'str.count' should be equal or greater to 'length'."); data := str.data; count := str.count; @@ -236,40 +240,44 @@ is_empty_string :: (str: string) -> bool { // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. -mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { +mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { // BUG Setting a timer to 99.9y shows 100y. 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; - if (time < 0) { + if time < 0 { return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, ""); } - else if (time == 0) { + else if time == 0 { return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, ""); } - else if (time < SECONDS_IN_MINUTE) { + else if time < SECONDS_IN_MINUTE { return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); } - else if (time < 100 * SECONDS_IN_HOUR) { + 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; return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); } - else if (time < xx (9999.5 * SECONDS_IN_DAY)) { + else if time < #run mul_f64_s64(9999.5, SECONDS_IN_DAY) { value := cast(float64) time / SECONDS_IN_DAY; decimals := - ifx time >= xx 99.95 * SECONDS_IN_DAY then 0 else - ifx time >= xx 9.995 * SECONDS_IN_DAY then 1 else + 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; return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); } - else if (time < xx (9999.5 * SECONDS_IN_YEAR)) { + else if time < #run mul_f64_s64(9999.5, SECONDS_IN_YEAR) { value := cast(float64) time / SECONDS_IN_YEAR; decimals := - ifx time >= xx 99.95 * SECONDS_IN_YEAR then 0 else - ifx time >= xx 9.995 * SECONDS_IN_YEAR then 1 else + 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; return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); } @@ -310,7 +318,7 @@ contains_task :: inline (db: Database, task: *Task) -> bool { // Adds a task to the database and returns it. // If necessary, expands database capacity. add_task :: (db: *Database, task: Task = .{}) -> task: *Task { - assert(db != null, "Parameter 'db' is null."); + assert(db != null, ASSERT_NOT_NULL, "db"); array_add(*db.tasks, task); for * db.total_times { @@ -324,10 +332,9 @@ add_task :: (db: *Database, task: Task = .{}) -> task: *Task { // If possible, shrinks the database capacity. // Returns success. delete_task :: (db: *Database, task: *Task) -> bool { - - assert(db != null); - assert(task != null); - assert(contains_task(db, task)); + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(task != null, ASSERT_NOT_NULL, "task"); + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); // Remove task timer values from total timers. for task.times { @@ -387,9 +394,9 @@ delete_task :: (db: *Database, task: *Task) -> bool { // Moves task to index. // Index gets clamped to [0, db->count[. move_task_to_index :: (db: *Database, task: *Task, index: s64) { - assert(db != null, "Parameter 'db' is null."); - assert(task != null, "Parameter 'task' is null."); - assert(contains_task(db, task), "Database does not contain task."); + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(task != null, ASSERT_NOT_NULL, "task"); + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); target_index := clamp(index, 0, db.tasks.count-1); target_task := *db.tasks[target_index]; @@ -430,7 +437,7 @@ move_task_to_index :: (db: *Database, task: *Task, index: s64) { // Updates the times on the active task (and adjusts database totals). update_times :: (db: *Database) { - assert(db != null); + assert(db != null, ASSERT_NOT_NULL, "db"); if db.active_idx < 0 return; @@ -467,7 +474,7 @@ update_times :: (db: *Database) { // Recalculates database totals. update_total_times :: (db: *Database) { - assert(db != null); + assert(db != null, ASSERT_NOT_NULL, "db"); totals: []s64 = db.total_times; memset(totals.data, 0, NUM_WEEK_DAYS * size_of(s64)); @@ -485,9 +492,9 @@ update_total_times :: (db: *Database) { // Resets the times of the provided task (and adjusts database totals). reset_task_times :: (db: *Database, task: *Task) { - assert(db != null); - assert(task != null); - assert(contains_task(db, task)); + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(task != null, ASSERT_NOT_NULL, "task"); + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); // Make sure we sync before applying the changes. update_times(db); @@ -497,40 +504,36 @@ reset_task_times :: (db: *Database, task: *Task) { <= db->tasks && task - db->tasks < db->count); +set_task_time :: (db: *Database, task: *Task, day: int, time: s64) { + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(task != null, ASSERT_NOT_NULL, "task"); + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); // Make sure we sync before applying the changes. update_times(db); - int64_t *timer = &task->times[day]; - int64_t *total = &db->total_times[day]; - *total = sub_int64(*total, *timer); - *timer = time; - *total = add_int64(*total, *timer); + db.total_times[day] = add_int64(db.total_times[day], time - task.times[day]); + task.times[day] = time; } // Adds the time on the day and task provided (and adjusts database totals). -void add_task_time(database_st *db, task_st *task, int day, int64_t time) { - assert(db != NULL); - assert(task != NULL); - assert(task >= db->tasks && task - db->tasks < db->count); +add_task_time :: (db: *Database, task: *Task, day: int, time: s64) { + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(task != null, ASSERT_NOT_NULL, "task"); + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); // Make sure we sync before applying the changes. update_times(db); - task->times[day] = add_int64(task->times[day], time); - db->total_times[day] = add_int64(db->total_times[day], time); + db.total_times[day] = add_int64(db.total_times[day], time); + task.times[day] = add_int64(task.times[day], time); } -*/ // Resets database to the initial state and deallocates all memory taken by tasks. reset_database :: (db: *Database) { - assert(db != null, "Parameter 'db' is null."); + assert(db != null, ASSERT_NOT_NULL, "db"); array_reset(*db.tasks); < success: bool { - assert(xx path, "Parameter 'path' is empty."); + assert(xx path, ASSERT_NOT_EMPTY, "path"); // Open file. file, open_success := file_open(path, for_writing = true); // log_errors: bool = true @@ -558,8 +561,8 @@ store_database :: (db: Database, path: string) -> success: bool { // Loads data from binary file into database. // Returns success. load_database :: (db: *Database, path: string) -> success: bool { - assert(db != null, "Parameter 'db' is null."); - assert(xx path, "Parameter 'path' is empty."); + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(xx path, ASSERT_NOT_EMPTY, "path"); // Open file. file, open_success := file_open(path); // log_errors: bool = true @@ -607,7 +610,7 @@ load_database :: (db: *Database, path: string) -> success: bool { // Exports data into CSV file. // Returns success. export_to_csv :: (db: Database, path: string) -> success: bool { - assert(xx path, "Parameter 'path' is empty."); + assert(xx path, ASSERT_NOT_EMPTY, "path"); // TODO Make sure (IN ALL PROCEDURES) we're not receiving an empty path. @@ -637,8 +640,8 @@ export_to_csv :: (db: Database, path: string) -> success: bool { // Returns success. import_from_csv :: (db: *Database, path: string) -> bool { // TODO Review code. - assert(db != null, "Parameter 'db' is null."); - assert(xx path, "Parameter 'path' is empty."); + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(xx path, ASSERT_NOT_EMPTY, "path"); error_code: s64; // Check file size TODO Read based on file size @@ -750,6 +753,9 @@ import_from_csv :: (db: *Database, path: string) -> bool { // } } } + + // Adjust selected task. + if (db.selected_idx < 0) db.selected_idx = 0; return true; } @@ -757,7 +763,7 @@ import_from_csv :: (db: *Database, path: string) -> bool { // Appends task to the end of the CSV file. // Returns success. append_to_csv :: (task: Task, path: string) -> success: bool { - assert(xx path, "Parameter 'path' is empty."); + assert(xx path, ASSERT_NOT_EMPTY, "path"); file, file_success := file_open(path, true, true); defer file_close(*file); @@ -785,7 +791,7 @@ append_to_csv :: (task: Task, path: string) -> success: bool { // Selects task by index. // Index gets clamped to [0, db->count[. select_task_by_index :: (db: *Database, index: s64) { - assert(db != null); + assert(db != null, ASSERT_NOT_NULL, "db"); db.selected_idx = ifx db.tasks.count == 0 then -1 else ifx index < 0 then 0 else ifx index >= db.tasks.count then db.tasks.count - 1 else @@ -794,7 +800,7 @@ select_task_by_index :: (db: *Database, index: s64) { // Selects task by delta relative to currently selected task. select_task_by_delta :: (db: *Database, delta: s64) { - assert(db != null); + assert(db != null, ASSERT_NOT_NULL, "db"); // TODO I bet there's a better way to do this... maybe use a clamp or range or something. idx := ifx (delta > 0 && db.selected_idx > S64_MAX - delta) then S64_MAX else @@ -803,19 +809,22 @@ select_task_by_delta :: (db: *Database, delta: s64) { select_task_by_index(db, idx); } + + // Selects task. select_task :: (db: *Database, task: *Task) { - assert(db != null); - assert(task != null); - assert(task >= db.tasks.data && task - db.tasks.data < db.tasks.count); + assert(db != null, ASSERT_NOT_NULL, "db"); + assert(task != null, ASSERT_NOT_NULL, "task"); + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); db.selected_idx = task - db.tasks.data; } // Set active task. // Passing task as NULL de-activates any previously active task. set_active_task :: (db: *Database, task: *Task) { - assert(db != null); - assert(task == null || (task >= db.tasks.data && task <= *db.tasks[db.tasks.count-1])); // TODO Improve this check. + assert(db != null, ASSERT_NOT_NULL, "db"); + if task != null + assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); update_times(db); db.active_idx = ifx (task == null) then -1 else task - db.tasks.data; } @@ -1160,7 +1169,7 @@ read_input_int :: (row: int, style: s32, message: string) -> value: int, success // Retuns true if user presses enter, false otherwise. read_enter_confirmation :: (row: int, style: int, message: string) -> bool { - assert(message.data != null); + assert(message.data != null, ASSERT_NOT_NULL, "message"); // TODO Improve this check? attron(xx style); move(xx row, 1); @@ -1451,93 +1460,76 @@ main :: () { delete_task(db, selected_task); trigger_autosave(); } -/* - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': { - if (selected_task == NULL) { - break; - } + + 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. - int selected_day = key - '1'; - int input_width = layout->columns[L_DAYS_IDX + selected_day].width; - int input_pos_x = 1 + layout->columns[L_TITLE_IDX].width; - for (int col = 0; col < selected_day; col++) { - input_pos_x += 1 + layout->columns[L_DAYS_IDX + col].width; + selected_day := key - #char "1"; + input_width := layout.columns[L_DAYS_IDX + selected_day].width; + input_pos_x := 1 + 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++; + input_pos_x += 1; // Get input string. - read_input_to_string_buffer(selected_task_row, input_pos_x, action_style, input_width); - char *input = string_buffer; + input := read_input_string(selected_task_row, input_pos_x, action_style, input_width); // TODO Temp stringzes. // Abort if input if empty. - if (is_empty_string(input) == true) { - break; - } + if is_empty_string(input) continue; - // Search for assign '=' operator and discard everything before (if found). - char *assign_str = strchr(input, '='); - bool is_assign = assign_str != NULL; - if (is_assign == true) { - input = assign_str + 1; - } + // 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. - char *parser; - long double input_float = strtold(input, &parser); - if (parser == input) { - break; - } - input = parser; + input_float, parse_success := string_to_float64(input); + if parse_success == false continue; // Try to parse a character representing the time multiplier. - long double multiplier = 1.0; - for (int i=0; i < strlen(input); i++) { - char ch = input[i]; - if (ch == 'm' || ch == 'M') { - multiplier = SECONDS_IN_MINUTE; - break; - } - else if (ch == 'h' || ch == 'H') { - multiplier = SECONDS_IN_HOUR; - break; - } - else if (ch == 'd' || ch == 'D') { - multiplier = SECONDS_IN_DAY; - break; - } - else if (ch == 'y' || ch == 'Y') { - multiplier = SECONDS_IN_YEAR; - break; + multiplier: float64 = 1.0; + for 0..input.count-1 { + ch := to_lower(input[it]); + if ch == { + case #char "m"; + multiplier = xx SECONDS_IN_MINUTE; + break; + + case #char "h"; + multiplier = xx SECONDS_IN_HOUR; + break; + + case #char "d"; + multiplier = xx SECONDS_IN_DAY; + break; + + case #char "y"; + multiplier = xx SECONDS_IN_YEAR; + break; } } // Process input and check if it's valid. - long double input_time = input_float * multiplier; - bool is_result_valid = (input_time >= (long double)INT64_MIN && input_time <= (long double)INT64_MAX); - if (is_result_valid == false) { - break; - } + input_time := input_float * multiplier; + if (input_time > xx S64_MAX || input_time < xx S64_MIN) continue; // Apply changes. - int64_t time = input_time; - int day = (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; - if (is_assign == true) { - set_task_time(db, selected_task, day, time); - } - else { - add_task_time(db, selected_task, day, time); - } + time := cast(s64)input_time; + day := (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + if is_assign set_task_time(db, selected_task, day, time); + else add_task_time(db, selected_task, day, time); + trigger_autosave(); - break; - } -*/ + case #char "m"; #through; case #char "M"; if selected_task == null continue; -- cgit v1.2.3 From 36c91454f2c5678bb648c23bcbc58feb12a006f7 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 18 Apr 2023 01:29:19 +0100 Subject: Using task index instead of pointer on helper functions. --- ttt.jai | 219 +++++++++++++++++++++++++++++----------------------------------- 1 file changed, 99 insertions(+), 120 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index bc81901..68e4e50 100644 --- a/ttt.jai +++ b/ttt.jai @@ -41,6 +41,7 @@ DB_FILE_SIGN_STR :: "TTT:B:02"; ASSERT_NOT_NULL :: "Parameter '%' is null."; ASSERT_NOT_EMPTY :: "Parameter '%' is empty."; ASSERT_NOT_CONTAIN :: "'%' does not contain '%'."; +ASSERT_INVALID_INDEX:: "Invalid index '%'."; SECONDS_IN_MINUTE :: cast(s64)60; SECONDS_IN_HOUR :: cast(s64)60*SECONDS_IN_MINUTE; @@ -282,7 +283,10 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { // BUG Setting return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); } else { - return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); + // TODO Solve unicode emoji gone wild. + //return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); + //return mvprintw(y, x, "%*s \xE2\x99\xBE %*s", left_padding, "", right_padding, ""); + return mvprintw(y, x, "%*s inf %*s", left_padding, "", right_padding, ""); } } @@ -310,14 +314,11 @@ get_selected_task :: inline (db: Database) -> *Task { return ifx db.selected_idx >= 0 then *db.tasks[db.selected_idx] else null; } -// TODO Add description. -contains_task :: inline (db: Database, task: *Task) -> bool { - return task >= db.tasks.data && task - db.tasks.data < db.tasks.count; -} +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 = .{}) -> task: *Task { +add_task :: (db: *Database, task: Task = .{}) -> task: *Task, index: s64 { assert(db != null, ASSERT_NOT_NULL, "db"); array_add(*db.tasks, task); @@ -325,47 +326,46 @@ add_task :: (db: *Database, task: Task = .{}) -> task: *Task { < bool { +delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `using db`. assert(db != null, ASSERT_NOT_NULL, "db"); - assert(task != null, ASSERT_NOT_NULL, "task"); - assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); + assert(is_valid_index(db, index), ASSERT_INVALID_INDEX, index); // Remove task timer values from total timers. - for task.times { - db.total_times[it_index] = sub_int64(db.total_times[it_index], it); + for tasks[index].times { + total_times[it_index] = sub_int64(total_times[it_index], it); } // Move tasks after the index position to their new positions. - index := task - db.tasks.data; - for index..db.tasks.count-2 - db.tasks[it] = db.tasks[it+1]; - db.tasks.count -= 1; + for index..tasks.count-2 + tasks[it] = tasks[it+1]; + tasks.count -= 1; // Adjust selected task. - if (db.selected_idx >= db.tasks.count) { - db.selected_idx -= 1; + if (selected_idx >= tasks.count) { + selected_idx -= 1; } // Adjust active task. - if (db.active_idx > index) { - db.active_idx -= 1; + if (active_idx > index) { + active_idx -= 1; } - else if (db.active_idx == index) { - db.active_idx = -1; + else if (active_idx == index) { + active_idx = -1; } // If possible, shrink database capacity. // TODO Do we really want to make this fuss? - current_capacity := db.tasks.allocated; - if (db.tasks.count < (current_capacity >> 2)) { - new_capacity := 1 << (get_msb(db.tasks.count) + 2); - my_array_reserve_nonpoly(xx *db.tasks, new_capacity, SIZE_OF_TASK); + current_capacity := tasks.allocated; + if (tasks.count < (current_capacity >> 2)) { + new_capacity := 1 << (get_msb(tasks.count) + 2); + my_array_reserve_nonpoly(xx *tasks, new_capacity, SIZE_OF_TASK); } get_msb :: (value: s64) -> msb: s64, found: bool { @@ -391,48 +391,42 @@ delete_task :: (db: *Database, task: *Task) -> bool { return true; } -// Moves task to index. -// Index gets clamped to [0, db->count[. -move_task_to_index :: (db: *Database, task: *Task, index: s64) { +// Moves task from source to target. +// Source and target get clamped to database size. +move_task :: (db: *Database, source: s64, target: s64) { // TODO Maybe `using db` assert(db != null, ASSERT_NOT_NULL, "db"); - assert(task != null, ASSERT_NOT_NULL, "task"); - assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); - - target_index := clamp(index, 0, db.tasks.count-1); - target_task := *db.tasks[target_index]; - - if (target_task == task) return; - - // Move task to new location. - temp_task := < task { - //memmove(task, task + 1, (target_task - task) * SIZEOF_TASK_ST); TODO Maybe simplify this moves. - offset := task - db.tasks.data; - size := target_task - task; - for 0..size-1 - db.tasks[offset + it] = db.tasks[offset + it + 1]; + + source = clamp(source, 0, db.tasks.count-1); + target = clamp(target, 0, db.tasks.count-1); + + if (source == target) return; + + // Move task to new location, but first, shift the others to allow some space. + temp_task := db.tasks[source]; + move_size := abs(target - source); + + if target > source { + for 0..move_size-1 + db.tasks[source + it] = db.tasks[source + it + 1]; } else { - //memmove(target_task + 1, target_task, (task - target_task) * SIZEOF_TASK_ST); TODO Maybe simplify this moves. - offset := target_task - db.tasks.data; - size := task - target_task; - for < size-1..0 - db.tasks[offset + it + 1] = db.tasks[offset + it]; + for < move_size-1..0 + db.tasks[target + it + 1] = db.tasks[target + it]; } - < success: bool { // Selects task by index. // Index gets clamped to [0, db->count[. -select_task_by_index :: (db: *Database, index: s64) { +select_task :: (db: *Database, index: s64) { assert(db != null, ASSERT_NOT_NULL, "db"); - db.selected_idx = ifx db.tasks.count == 0 then -1 else - ifx index < 0 then 0 else - ifx index >= db.tasks.count then db.tasks.count - 1 else - index; + 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"); - // TODO I bet there's a better way to do this... maybe use a clamp or range or something. 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_by_index(db, idx); -} - - - -// Selects task. -select_task :: (db: *Database, task: *Task) { - assert(db != null, ASSERT_NOT_NULL, "db"); - assert(task != null, ASSERT_NOT_NULL, "task"); - assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); - db.selected_idx = task - db.tasks.data; + select_task(db, idx); } // Set active task. -// Passing task as NULL de-activates any previously active task. -set_active_task :: (db: *Database, task: *Task) { +// Passing -1 de-activates any previously active task. +set_active_task :: (db: *Database, index: s64) { assert(db != null, ASSERT_NOT_NULL, "db"); - if task != null - assert(contains_task(db, task), ASSERT_NOT_CONTAIN, "db", "task"); + assert(index == -1 || is_valid_index(db, index), ASSERT_INVALID_INDEX, index); update_times(db); - db.active_idx = ifx (task == null) then -1 else task - db.tasks.data; + db.active_idx = index; } // Returns true when database is full. @@ -1377,17 +1353,23 @@ main :: () { if key == #char "q" || key == #char "Q" break; update_times(*database); timeout(INPUT_AWAIT_INF); - - active_task := get_active_task(db); + + // TODO WIP Remove `selected_task` and `active_task` and helper functions. selected_task := get_selected_task(db); - action_style := A_BOLD | COLOR_PAIR(xx ifx selected_task == active_task && selected_task != null - then Styles.ACTIVE - else Styles.SELECTED_INVERTED); - error_style := A_BOLD | COLOR_PAIR(xx Styles.ERROR); - selected_task_row : int = ifx is_terminal_too_small then 0 - else ifx (db.selected_idx < 0) then 1 - else (db.selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS; - + active_task := get_active_task(db); + action_style: s32; + error_style: s32; + selected_task_row: int; + { // TODO Recheck this code. + using db; + action_style = A_BOLD | COLOR_PAIR(xx + ifx selected_idx == active_idx && selected_idx != -1 then Styles.ACTIVE + else Styles.SELECTED_INVERTED); + error_style = A_BOLD | COLOR_PAIR(xx Styles.ERROR); + selected_task_row = ifx is_terminal_too_small then 0 + else ifx (selected_idx < 0) then 1 + else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS; + } if key == { // When getch() times out. @@ -1422,11 +1404,11 @@ main :: () { now_utc := current_time_consensus(); now_local := to_calendar(now_utc, .LOCAL); name := calendar_to_iso_string(now_local); - new_task := add_task(db); - memcpy(new_task.name.data, name.data, min(Task.name.count, name.count)); + task, index := add_task(db); + memcpy(task.name.data, name.data, min(Task.name.count, name.count)); // Select new task. - select_task(db, new_task); + select_task(db, index); selected_task = get_selected_task(db); trigger_autosave(); @@ -1449,7 +1431,7 @@ main :: () { if (selected_task == null) continue; if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) { - reset_task_times(db, selected_task); + reset_task_times(db, db.selected_idx); trigger_autosave(); } @@ -1457,7 +1439,7 @@ main :: () { if (selected_task == null || selected_task == active_task) continue; if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) { - delete_task(db, selected_task); + delete_task(db, db.selected_idx); trigger_autosave(); } @@ -1525,8 +1507,8 @@ main :: () { // Apply changes. time := cast(s64)input_time; day := (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; - if is_assign set_task_time(db, selected_task, day, time); - else add_task_time(db, selected_task, day, time); + if is_assign set_task_time(db, db.selected_idx, day, time); + else add_task_time(db, db.selected_idx, day, time); trigger_autosave(); @@ -1536,9 +1518,7 @@ main :: () { value, success := read_input_int(selected_task_row, action_style, " Move to: "); if success == false continue; - - target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1; - move_task_to_index(db, selected_task, target_index); + move_task(db, db.selected_idx, value-1); // -1 to adjust for zero based index trigger_autosave(); case #char "g"; #through; @@ -1547,9 +1527,8 @@ main :: () { value, success := read_input_int(selected_task_row, action_style, " Go to: "); if success == false continue; - target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1; - select_task_by_index(db, target_index); + select_task(db, target_index); case #char "d"; #through; case #char "D"; @@ -1571,12 +1550,12 @@ main :: () { case #char "t"; #through; case #char "T"; if (active_task == null) continue; - select_task(db, active_task); + select_task(db, db.active_idx); case #char "\n"; #through; case #char " "; if (db != *database || selected_task == null) continue; - set_active_task(db, ifx (active_task == selected_task) then null else selected_task); + 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(); @@ -1606,7 +1585,7 @@ main :: () { print_error("Failed to archive entry."); continue; } - delete_task(*database, selected_task); + delete_task(db, db.selected_idx); trigger_autosave(); case #char "r"; #through; @@ -1622,11 +1601,11 @@ main :: () { print_error("Failed to restore entry."); continue; } - delete_task(*archive, selected_task); + delete_task(db, db.selected_idx); trigger_autosave(); case KEY_HOME; - select_task_by_index(db, 0); + select_task(db, 0); case KEY_UP; select_task_by_delta(db, -1); @@ -1635,7 +1614,7 @@ main :: () { select_task_by_delta(db, -layout_tasks_rows); case KEY_END; - select_task_by_index(db, db.tasks.count-1); + select_task(db, db.tasks.count-1); case KEY_DOWN; select_task_by_delta(db, 1); -- cgit v1.2.3 From fde8632915efd5c0f3233c65ea4e1d9191023b0f Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 18 Apr 2023 17:25:40 +0100 Subject: Fix print of task names on LLVM. --- ttt.jai | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 68e4e50..5c8e950 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1013,7 +1013,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Display up to rows allowed by the layout, or less if reached end of database. idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); for task_idx: idx_start..idx_stop-1 { - //for (size_t idx = idx_start; idx < idx_stop; idx++) { + auto_release_temp(); // TODO Temporary memory being trashed?! task := *db.tasks[task_idx]; y += 1; x = 0; @@ -1032,7 +1032,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Task title. x += 1; column_width = layout.columns[L_TITLE_IDX].width; - mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, task.name); + mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. x += column_width; // Task times. @@ -1354,7 +1354,7 @@ main :: () { update_times(*database); timeout(INPUT_AWAIT_INF); - // TODO WIP Remove `selected_task` and `active_task` and helper functions. + // TODO WIP Remove `selected_task` and `active_task` and helper functions. selected_task := get_selected_task(db); active_task := get_active_task(db); action_style: s32; -- cgit v1.2.3 From adff8aaceb63d3e679f0f5cd4c5f93146ad8f012 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 19 Apr 2023 00:08:32 +0100 Subject: Fixed bugs on import_from_csv. --- ttt.jai | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 5c8e950..3822d72 100644 --- a/ttt.jai +++ b/ttt.jai @@ -241,7 +241,7 @@ is_empty_string :: (str: string) -> bool { // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. -mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { // BUG Setting a timer to 99.9y shows 100y. +mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { TIME_CHARS :: 5; assert(space >= TIME_CHARS); @@ -730,11 +730,8 @@ import_from_csv :: (db: *Database, path: string) -> bool { truncate_string(xx task.name, name_length); advance(*csv_values); - for csv_values { - parsed_value := string_to_int(it); - task.times[it_index] = parsed_value; - db.total_times[it_index] = add_int64(db.total_times[it_index], parsed_value); - } + for csv_values + task.times[it_index] = string_to_int(it); add_task(db, *task); // TODO Check this old code and remove it if not necessary. @@ -746,7 +743,7 @@ import_from_csv :: (db: *Database, path: string) -> bool { } // Adjust selected task. - if (db.selected_idx < 0) db.selected_idx = 0; + if (db.selected_idx < 0 && db.tasks.count > 0) db.selected_idx = 0; return true; } -- cgit v1.2.3 From 68be730ffb762e2a0d796fc57d6039ad75cd010e Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 19 Apr 2023 00:09:23 +0100 Subject: Implemented action to backup and reset all tasks. --- ttt.jai | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 3822d72..df02123 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1158,6 +1158,8 @@ read_enter_confirmation :: (row: int, style: int, message: string) -> bool { main :: () { + // TODO Implement signal handling and see modules/Debug.jai for examples. + defer report_memory_leaks(); // TODO Remove after final debug sessions. defer free_memory(); @@ -1227,6 +1229,7 @@ main :: () { " m, M Move selected task to position.\n", " g, G Select task by position.\n", " q, Q Save changes and exit.\n", + " w, W Archive duplicates and reset all tasks.\n", // TODO Improve message. " F2 Rename selected task.\n", " F5 Recalculate total times.\n", " TAB Toggle archive view.\n", @@ -1601,6 +1604,19 @@ main :: () { delete_task(db, db.selected_idx); trigger_autosave(); + case #char "w"; #through; + case #char "W"; + if (db != *database || db.tasks.count <= 0) continue; + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to archive duplicates and reset all. ") == true) { // TODO Improve message. + for db.tasks { + if (append_to_csv(it, ar_file_path) == false) { + print_error("Failed to archive entry."); // TODO Improve this. + } + reset_task_times(db, it_index); + } + trigger_autosave(); + } + case KEY_HOME; select_task(db, 0); @@ -1644,5 +1660,6 @@ main :: () { } endwin(); + exit(xx ifx error_saving then 1 else 0); } -- cgit v1.2.3 From 36a63f99c636f12b4b5c05edde5603f9bc8930e0 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 20 Apr 2023 00:00:24 +0100 Subject: Implemented invert tasks order feature. --- ttt.jai | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index df02123..cd3d46e 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1228,6 +1228,7 @@ main :: () { " 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", " q, Q Save changes and exit.\n", " w, W Archive duplicates and reset all tasks.\n", // TODO Improve message. " F2 Rename selected task.\n", @@ -1393,6 +1394,20 @@ main :: () { update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; + 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; + case #char "n"; #through; case #char "N"; if is_database_full(db) { -- cgit v1.2.3 From 09bd5e304c940f9abcb68fc47fa955043ed9e750 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 20 Apr 2023 01:29:56 +0100 Subject: Implemented workspace cleanup and sort by features. --- curses.jai | 4 ++-- ttt.jai | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 60 insertions(+), 7 deletions(-) (limited to 'ttt.jai') diff --git a/curses.jai b/curses.jai index 1f62262..7ec39c0 100644 --- a/curses.jai +++ b/curses.jai @@ -114,8 +114,8 @@ mvwprintw :: (win: *WINDOW, y: s32, x: s32, fmt: *u8, args: ..Any) -> s32 #foreign ncurses; wmove :: (win: *WINDOW, y: s32, x: s32) -> s32 #foreign ncurses; waddch :: (win: *WINDOW, ch: u32) -> s32 #foreign ncurses; -vw_printw :: (win: *WINDOW, fmt: *u8, - varglist: ..Any) -> s32 #foreign ncurses; +wprintw :: (win: *WINDOW, fmt: *u8, + args: ..Any) -> s32 #foreign ncurses; getmaxyx :: inline (win: *WINDOW, y: *s32, x: *s32) { < value: int, success } // Retuns true if user presses enter, false otherwise. -read_enter_confirmation :: (row: int, style: int, message: string) -> bool { +read_input_char :: (row: int, style: int, message: string) -> s32 { assert(message.data != null, ASSERT_NOT_NULL, "message"); // TODO Improve this check? attron(xx style); move(xx row, 1); for 0..size_x-3 { - //for (int idx = 0; idx < size_x - 2; idx++) { + //for (int idx = 0; idx < size_x - 2; idx++) { // TODO check what's going on here. addch(ACS_CKBOARD); } mvaddstr(xx row, 2, message.data); attrset(A_NORMAL); - return getch() == #char "\n"; + return getch(); +} + +// Retuns true if user presses enter, false otherwise. +read_enter_confirmation :: inline (row: int, style: int, message: string) -> bool { + return read_input_char(row, style, message) == #char "\n"; } main :: () { @@ -1221,6 +1227,7 @@ main :: () { " -v, --version Output version information and exit.\n", "\n", "In app commands\n", + " w, W Archive a duplicate and reset times for 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", @@ -1229,8 +1236,11 @@ main :: () { " 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", - " w, W Archive duplicates and reset all tasks.\n", // TODO Improve message. " F2 Rename selected task.\n", " F5 Recalculate total times.\n", " TAB Toggle archive view.\n", @@ -1407,6 +1417,7 @@ main :: () { } if db.active_idx >= 0 db.active_idx = count - db.active_idx; + trigger_autosave(); case #char "n"; #through; case #char "N"; @@ -1603,6 +1614,7 @@ main :: () { delete_task(db, db.selected_idx); trigger_autosave(); + // Restore archived task. case #char "r"; #through; case #char "R"; if (db != *archive || selected_task == null) continue; @@ -1619,6 +1631,47 @@ main :: () { delete_task(db, db.selected_idx); trigger_autosave(); + // Sort by. + case #char "s"; #through; + case #char "S"; + sort_by := read_input_char(selected_task_row, action_style, " Sort by (n) name, (1..7) day, or (t) total time. "); + if sort_by == { + case #char "n"; #through; + case #char "N"; + quick_sort(db.tasks, (x, y) => compare_strings(xx x.name, xx y.name)); + + case #char "t"; #through; + case #char "T"; + compare_tasks :: (x: Task, y: Task) -> s64 { + total_x, total_y: s64; + for x.times { total_x = add_int64(total_x, it); }; + for y.times { total_y = add_int64(total_y, it); }; + return sub_int64(total_x, total_y); + }; + quick_sort(db.tasks, compare_tasks); + + 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 := sort_by - #char "1"; + day := (sort_by_idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + if day == { + case 0; quick_sort(db.tasks, (x, y) => x.times[0] - y.times[0]); + case 1; quick_sort(db.tasks, (x, y) => x.times[1] - y.times[1]); + case 2; quick_sort(db.tasks, (x, y) => x.times[2] - y.times[2]); + case 3; quick_sort(db.tasks, (x, y) => x.times[3] - y.times[3]); + case 4; quick_sort(db.tasks, (x, y) => x.times[4] - y.times[4]); + case 5; quick_sort(db.tasks, (x, y) => x.times[5] - y.times[5]); + case 6; quick_sort(db.tasks, (x, y) => x.times[6] - y.times[6]); + } + } + trigger_autosave(); + + // Workspace cleanup. case #char "w"; #through; case #char "W"; if (db != *database || db.tasks.count <= 0) continue; -- cgit v1.2.3 From 7ff77e19362bf0c01e7ba34df5dd968bd9d14b3d Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 20 Apr 2023 01:31:45 +0100 Subject: NOTE: The sort by feature has (at least) one bug. --- ttt.jai | 1 + 1 file changed, 1 insertion(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 3b5cb4c..952de6a 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1634,6 +1634,7 @@ main :: () { // Sort by. case #char "s"; #through; case #char "S"; + // BUG FIXME The `sort by` feature does not keep track of the currently active task. sort_by := read_input_char(selected_task_row, action_style, " Sort by (n) name, (1..7) day, or (t) total time. "); if sort_by == { case #char "n"; #through; -- cgit v1.2.3 From 804dd0a008b9e3ecaecae217cbbd18c7d6d5a502 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 20 Apr 2023 09:43:41 +0100 Subject: Fixed bug on sort by feature: now it keeps track of active task. --- ttt.jai | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 952de6a..b2639d9 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1634,7 +1634,8 @@ main :: () { // Sort by. case #char "s"; #through; case #char "S"; - // BUG FIXME The `sort by` feature does not keep track of the currently active task. + // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. + active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{}; sort_by := read_input_char(selected_task_row, action_style, " Sort by (n) name, (1..7) day, or (t) total time. "); if sort_by == { case #char "n"; #through; @@ -1670,6 +1671,25 @@ main :: () { case 6; quick_sort(db.tasks, (x, y) => x.times[6] - y.times[6]); } } + if db.active_idx >= 0 { + + 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 active_task.name, xx it.name) == 0 && compare_array(active_task.times, it.times) == 0 { + db.active_idx = it_index; + break; + } + } + } trigger_autosave(); // Workspace cleanup. -- cgit v1.2.3 From 1d0c89ead1dc18ade2051e80b194b9de290106d5 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 21 Apr 2023 00:04:51 +0100 Subject: Fixed bugs on update_times. --- ttt.jai | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index b2639d9..9c7f948 100644 --- a/ttt.jai +++ b/ttt.jai @@ -434,11 +434,14 @@ move_task :: (db: *Database, source: s64, target: s64) { // TODO Maybe `using db update_times :: (db: *Database) { assert(db != null, ASSERT_NOT_NULL, "db"); - if db.active_idx < 0 return; - // Get time frame in UTC. start_time := db.modified_on; - stop_time := current_time_consensus(); + 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; + + if db.active_idx < 0 return; active_task := *db.tasks[db.active_idx]; start_week_day: s8; @@ -462,9 +465,6 @@ update_times :: (db: *Database) { start_time = next_start; } - - // Keep track of this update. - db.modified_on = stop_time; } // Recalculates database totals. -- cgit v1.2.3 From 8b5c33cd6f2ff1f0a073a773a27987b995ea8bc1 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 29 Apr 2023 03:48:10 +0100 Subject: Prototyping sat_s64 add/sub procedures. --- ttt.jai | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 9c7f948..60db8b7 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1163,6 +1163,95 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo } main :: () { + + // value_a: s64 = 2; + // value_b: s64 = S64_MAX-1; + value_a: s64 = -2; + value_b: s64 = S64_MIN+1; + print(">%\n", S64_MAX); + argx := get_command_line_arguments(); + if argx.count > 1 + value_a = parse_int(*argx[1]); + if argx.count > 2 + value_b = parse_int(*argx[2]); + + result: s64 = ---; + flags: s64;// = ---; + #asm LAHF_SAHF { // TODO Remove LAHF_SAHF it not required. + + // Code from https://locklessinc.com/articles/sat_arithmetic/ + + + // value_a === b; + // value_b === c; + // add value_a, value_b; + // mov result, value_a; + + // Version 1 + // mov b: gpr === b, value_a; + // mov c: gpr === c, value_b; + // add b, c; + // mov result, b; + // seto flags; + // cmovns b, c; + +// s64b sat_adds64b(s64b x, s64b y) +// { +// u64b ux = x; +// u64b uy = y; +// u64b res = ux + uy; +// +// ux = (ux >> 63) + LONG_MAX; +// +// /* Force compiler to use cmovns instruction */ +// if ((s64b) ((ux ^ uy) | ~(uy ^ res)) >= 0) +// { +// res = ux; +// } +// +// return res; +// } + // Version 2 - WORKS + mov d: gpr === d, 9223372036854775807; + mov a: gpr === a, value_a; + mov b: gpr === b, value_b; + add a, b; + seto flags; // Signal overflow. + mov result, a; + mov a, value_a; + shr a, 63; + add a, d; + mov c: gpr, value_a; + xor c, b; + xor b, result; + not b; + or c, b; + test c, c; + cmovns result, a; + +// s64b sat_subs64b(s64b x, s64b y) +// { +// u64b ux = x; +// u64b uy = y; +// u64b res = ux - uy; +// +// ux = (ux >> 63) + LONG_MAX; +// +// /* Force compiler to use cmovns instruction */ +// if ((s64b)((ux ^ uy) & (ux ^ res)) < 0) +// { +// res = ux; +// } +// +// return res; +// } + // TODO Use https://godbolt.org/ to help + + } + print("% + % = %\n", value_a, value_b, result); + print("flag: %\n", flags); + return; + // TODO Implement signal handling and see modules/Debug.jai for examples. -- cgit v1.2.3 From f993b20d0f4400d1b9e4c918f5cbbcb3cddbb8a0 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 29 Apr 2023 04:07:38 +0100 Subject: Comparing add_s64 implementations. --- ttt.jai | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 60db8b7..503c418 100644 --- a/ttt.jai +++ b/ttt.jai @@ -291,7 +291,7 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { } } -add_int64 :: (x :s64, y: s64) -> s64 { +add_int64 :: (x :s64, y: s64) -> s64 #dump { // TODO Comparing implementations. return ifx (y > 0 && x > S64_MAX - y) then S64_MAX else ifx (y < 0 && x < S64_MIN - y) then S64_MIN else @@ -1164,6 +1164,32 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo main :: () { + + add :: (value_a: s64, value_b: s64) -> s64, bool #dump { // TODO Comparing implementaitons using dump + result: s64 = ---; + flag: bool = ---; + #asm { + mov d: gpr === d, 9223372036854775807; + mov a: gpr === a, value_a; + mov b: gpr === b, value_b; + add a, b; + seto flag; // Signal overflow. + mov result, a; + mov a, value_a; + shr a, 63; + add a, d; + mov c: gpr, value_a; + xor c, b; + xor b, result; + not b; + or c, b; + test c, c; + cmovns result, a; + } + return result, flag; + } + + // value_a: s64 = 2; // value_b: s64 = S64_MAX-1; value_a: s64 = -2; -- cgit v1.2.3 From e39cc9078a253cf5b2adc6b0de11b54e7934bc9b Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 2 May 2023 17:42:22 +0100 Subject: Prototyping branchless integer saturated arithmetics. --- Math_Ext.jai | 172 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ttt.jai | 120 +---------------------------------------- 2 files changed, 174 insertions(+), 118 deletions(-) create mode 100644 Math_Ext.jai (limited to 'ttt.jai') diff --git a/Math_Ext.jai b/Math_Ext.jai new file mode 100644 index 0000000..5a02756 --- /dev/null +++ b/Math_Ext.jai @@ -0,0 +1,172 @@ +#import "Basic"; +#import "Compiler"; +#import "Math"; + +#run test_math_ext(); + +test_math_ext :: () { + + set_build_options_dc(.{do_output=false}); + + write_strings("=====================\n", "--- Test Math_Ext ---\n"); + + Test_Inputs :: struct(ia: $T, ib: T, ir: T, is: bool) { + // t: Type; + a := ia; + b := ib; + r := ir; + s := is; + }; + +/* + tests := Test_Inputs.[ + // .{1, 2, 3, false}, + ]; + + for * tests { + result, saturated := add(cast(it.t)it.a, cast(it.t)it.b); + assert(result == it.r && saturated == it.s, "Failed: % %\n", result, saturated); + } +*/ + + + t1a := S64_MAX; + t1b := 0; + t1v, t1r := add(t1a, t1b); + assert(t1v == S64_MAX && t1r == false, "Failed: % %\n", t1v, t1r); + write_string("t1: OK\n"); + + // t2a := S64_MAX; + // t2b := 1; + // t2v, t2r := add(t2a, t2b); + // assert(t2v == S64_MAX && t2r == true, "Failed: % %\n", t2v, t2r); +} + + +add_int64 :: (x :s64, y: s64) -> s64 #dump { // TODO Comparing implementations. + return + ifx (y > 0 && x > S64_MAX - y) then S64_MAX else + ifx (y < 0 && x < S64_MIN - y) then S64_MIN else + x + y; +} + +sub_int64 :: (x :s64, y :s64) -> s64 { + return + ifx (y < 0 && x > S64_MAX + y) then S64_MAX else + ifx (y > 0 && x < S64_MIN + y) then S64_MIN else + x - y; +} + +add :: (value_a: s64, value_b: s64) -> result: s64, saturated: bool #dump { // TODO Comparing implementaitons using dump + result: s64 = ---; + flag: bool = ---; + #asm { + mov d: gpr === d, 9223372036854775807; + mov a: gpr === a, value_a; + mov b: gpr === b, value_b; + add a, b; + seto flag; // Flag overflow. + mov result, a; + mov a, value_a; + shr a, 63; + add a, d; + mov c: gpr, value_a; + xor c, b; + xor b, result; + not b; + or c, b; + test c, c; + cmovns result, a; + } + return result, flag; +} + +/* +// value_a: s64 = 2; +// value_b: s64 = S64_MAX-1; +value_a: s64 = -2; +value_b: s64 = S64_MIN+1; +print(">%\n", S64_MAX); +argx := get_command_line_arguments(); +if argx.count > 1 + value_a = parse_int(*argx[1]); +if argx.count > 2 + value_b = parse_int(*argx[2]); + +result: s64 = ---; +flags: s64;// = ---; +#asm LAHF_SAHF { // TODO Remove LAHF_SAHF it not required. + + // Code from https://locklessinc.com/articles/sat_arithmetic/ + + + // value_a === b; + // value_b === c; + // add value_a, value_b; + // mov result, value_a; + + // Version 1 + // mov b: gpr === b, value_a; + // mov c: gpr === c, value_b; + // add b, c; + // mov result, b; + // seto flags; + // cmovns b, c; + +// s64b sat_adds64b(s64b x, s64b y) +// { +// u64b ux = x; +// u64b uy = y; +// u64b res = ux + uy; +// +// ux = (ux >> 63) + LONG_MAX; +// +// /* Force compiler to use cmovns instruction */ +// if ((s64b) ((ux ^ uy) | ~(uy ^ res)) >= 0) +// { +// res = ux; +// } +// +// return res; +// } + // Version 2 - WORKS + mov d: gpr === d, 9223372036854775807; + mov a: gpr === a, value_a; + mov b: gpr === b, value_b; + add a, b; + seto flags; // Flag overflow. + mov result, a; + mov a, value_a; + shr a, 63; + add a, d; + mov c: gpr, value_a; + xor c, b; + xor b, result; + not b; + or c, b; + test c, c; + cmovns result, a; + +// s64b sat_subs64b(s64b x, s64b y) +// { +// u64b ux = x; +// u64b uy = y; +// u64b res = ux - uy; +// +// ux = (ux >> 63) + LONG_MAX; +// +// // Force compiler to use cmovns instruction +// if ((s64b)((ux ^ uy) & (ux ^ res)) < 0) +// { +// res = ux; +// } +// +// return res; +// } + // TODO Use https://godbolt.org/ to help + +} +print("% + % = %\n", value_a, value_b, result); +print("flag: %\n", flags); +return; +*/ \ No newline at end of file diff --git a/ttt.jai b/ttt.jai index 503c418..554884a 100644 --- a/ttt.jai +++ b/ttt.jai @@ -474,6 +474,7 @@ update_total_times :: (db: *Database) { totals: []s64 = db.total_times; memset(totals.data, 0, NUM_WEEK_DAYS * size_of(s64)); for db.tasks { + // TODO Try to use local variables instead of total sub...something... the indexes thingy. times : []s64 = it.times; totals[0] = add_int64(totals[0], times[0]); totals[1] = add_int64(totals[1], times[1]); @@ -1163,121 +1164,6 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo } main :: () { - - - add :: (value_a: s64, value_b: s64) -> s64, bool #dump { // TODO Comparing implementaitons using dump - result: s64 = ---; - flag: bool = ---; - #asm { - mov d: gpr === d, 9223372036854775807; - mov a: gpr === a, value_a; - mov b: gpr === b, value_b; - add a, b; - seto flag; // Signal overflow. - mov result, a; - mov a, value_a; - shr a, 63; - add a, d; - mov c: gpr, value_a; - xor c, b; - xor b, result; - not b; - or c, b; - test c, c; - cmovns result, a; - } - return result, flag; - } - - - // value_a: s64 = 2; - // value_b: s64 = S64_MAX-1; - value_a: s64 = -2; - value_b: s64 = S64_MIN+1; - print(">%\n", S64_MAX); - argx := get_command_line_arguments(); - if argx.count > 1 - value_a = parse_int(*argx[1]); - if argx.count > 2 - value_b = parse_int(*argx[2]); - - result: s64 = ---; - flags: s64;// = ---; - #asm LAHF_SAHF { // TODO Remove LAHF_SAHF it not required. - - // Code from https://locklessinc.com/articles/sat_arithmetic/ - - - // value_a === b; - // value_b === c; - // add value_a, value_b; - // mov result, value_a; - - // Version 1 - // mov b: gpr === b, value_a; - // mov c: gpr === c, value_b; - // add b, c; - // mov result, b; - // seto flags; - // cmovns b, c; - -// s64b sat_adds64b(s64b x, s64b y) -// { -// u64b ux = x; -// u64b uy = y; -// u64b res = ux + uy; -// -// ux = (ux >> 63) + LONG_MAX; -// -// /* Force compiler to use cmovns instruction */ -// if ((s64b) ((ux ^ uy) | ~(uy ^ res)) >= 0) -// { -// res = ux; -// } -// -// return res; -// } - // Version 2 - WORKS - mov d: gpr === d, 9223372036854775807; - mov a: gpr === a, value_a; - mov b: gpr === b, value_b; - add a, b; - seto flags; // Signal overflow. - mov result, a; - mov a, value_a; - shr a, 63; - add a, d; - mov c: gpr, value_a; - xor c, b; - xor b, result; - not b; - or c, b; - test c, c; - cmovns result, a; - -// s64b sat_subs64b(s64b x, s64b y) -// { -// u64b ux = x; -// u64b uy = y; -// u64b res = ux - uy; -// -// ux = (ux >> 63) + LONG_MAX; -// -// /* Force compiler to use cmovns instruction */ -// if ((s64b)((ux ^ uy) & (ux ^ res)) < 0) -// { -// res = ux; -// } -// -// return res; -// } - // TODO Use https://godbolt.org/ to help - - } - print("% + % = %\n", value_a, value_b, result); - print("flag: %\n", flags); - return; - // TODO Implement signal handling and see modules/Debug.jai for examples. @@ -1293,9 +1179,7 @@ main :: () { } home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. - - - + if success_path == false { print_error("Failed to find home directory '%'.", home_dir); exit(1); -- cgit v1.2.3 From deaa914add9200fe66c09ea930c367ed190a46aa Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 9 Jul 2023 20:47:14 +0100 Subject: Now using saturating arithmetic lib. Fix jai breaking changes (no longer possible to use forward declared procedures). --- libncurses.so | Bin 165808 -> 165808 bytes ttt.jai | 105 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 54 insertions(+), 51 deletions(-) (limited to 'ttt.jai') diff --git a/libncurses.so b/libncurses.so index 1e7352e..46767e4 100644 Binary files a/libncurses.so and b/libncurses.so differ diff --git a/ttt.jai b/ttt.jai index 554884a..f3cc372 100644 --- a/ttt.jai +++ b/ttt.jai @@ -26,6 +26,7 @@ #import "File_Utilities"; #import "String"; #import "curses"; +#load "Integer_Saturating_Arithmetic.jai"; VERSION :: "2.0"; // Use only 3 chars (to fit layouts). @@ -34,7 +35,7 @@ FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). NUM_WEEK_DAYS :: 7; // TODO This has to go - Just to be more clear about what we're looping about. NAME_SIZE :: 72; // TODO Use this instead of Task.name.count ? -APP_FOLDER_NAME :: ".task_time_tracker_v2"; // TODO Using _v2 to avoid erasing my work data. +APP_FOLDER_NAME :: ".task_time_tracker_test"; // TODO Using _v2 to avoid erasing my work data. DB_FILE_NAME :: "database.bin"; AR_FILE_NAME :: "archive.csv"; DB_FILE_SIGN_STR :: "TTT:B:02"; @@ -291,20 +292,6 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { } } -add_int64 :: (x :s64, y: s64) -> s64 #dump { // TODO Comparing implementations. - return - ifx (y > 0 && x > S64_MAX - y) then S64_MAX else - ifx (y < 0 && x < S64_MIN - y) then S64_MIN else - x + y; -} - -sub_int64 :: (x :s64, y :s64) -> s64 { - return - ifx (y < 0 && x > S64_MAX + y) then S64_MAX else - ifx (y > 0 && x < S64_MIN + y) then S64_MIN else - x - y; -} - // 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; @@ -324,7 +311,7 @@ add_task :: (db: *Database, task: Task = .{}) -> task: *Task, index: s64 { array_add(*db.tasks, task); for * db.total_times { - < bool { // TODO Maybe use `us // Remove task timer values from total timers. for tasks[index].times { - total_times[it_index] = sub_int64(total_times[it_index], it); + total_times[it_index] = sub(total_times[it_index], it); } // Move tasks after the index position to their new positions. @@ -360,15 +347,8 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us else if (active_idx == index) { active_idx = -1; } - - // If possible, shrink database capacity. - // TODO Do we really want to make this fuss? - current_capacity := tasks.allocated; - if (tasks.count < (current_capacity >> 2)) { - new_capacity := 1 << (get_msb(tasks.count) + 2); - my_array_reserve_nonpoly(xx *tasks, new_capacity, SIZE_OF_TASK); - } - + + // TODO Helper function. get_msb :: (value: s64) -> msb: s64, found: bool { result: s64 = ---; #asm { @@ -377,7 +357,7 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us } return result * xx cast(bool)value, xx value; // If value is zero: return `0, false`. } - + my_array_reserve_nonpoly :: (array: *[..] *void, desired_items: s64, size: s64) -> success: bool { if !array.allocator.proc remember_allocators(array); @@ -389,6 +369,14 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us return true; } + // If possible, shrink database capacity. + // TODO Do we really want to make this fuss? + current_capacity := tasks.allocated; + if (tasks.count < (current_capacity >> 2)) { + new_capacity := 1 << (get_msb(tasks.count) + 2); + my_array_reserve_nonpoly(xx *tasks, new_capacity, SIZE_OF_TASK); + } + return true; } @@ -476,13 +464,13 @@ update_total_times :: (db: *Database) { for db.tasks { // TODO Try to use local variables instead of total sub...something... the indexes thingy. times : []s64 = it.times; - totals[0] = add_int64(totals[0], times[0]); - totals[1] = add_int64(totals[1], times[1]); - totals[2] = add_int64(totals[2], times[2]); - totals[3] = add_int64(totals[3], times[3]); - totals[4] = add_int64(totals[4], times[4]); - totals[5] = add_int64(totals[5], times[5]); - totals[6] = add_int64(totals[6], times[6]); + totals[0] = add(totals[0], times[0]); + totals[1] = add(totals[1], times[1]); + totals[2] = add(totals[2], times[2]); + totals[3] = add(totals[3], times[3]); + totals[4] = add(totals[4], times[4]); + totals[5] = add(totals[5], times[5]); + totals[6] = add(totals[6], times[6]); } } @@ -495,7 +483,7 @@ reset_task_times :: (db: *Database, index: s64) { update_times(db); for * db.tasks[index].times { - db.total_times[it_index] = sub_int64(db.total_times[it_index], < bool { print_error("Failed to read file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } + + + // TODO Helper function. + advance :: inline (array: *[] $T, amount: int = 1) { + assert(amount >= 0); + assert(array.count >= amount); + array.count -= amount; + array.data += amount; + } + // TODO Helper function. consume_next_line :: (sp: *string) -> string, bool { @@ -694,14 +692,6 @@ import_from_csv :: (db: *Database, path: string) -> bool { //Skip header line. consume_next_line(*csv); - // TODO Helper function. - advance :: inline (array: *[] $T, amount: int = 1) { - assert(amount >= 0); - assert(array.count >= amount); - array.count -= amount; - array.data += amount; - } - next_line :: inline (csv: *string) -> line: string, success: bool { for 0..csv.count { if csv.data[it] == #char "\n" { @@ -1041,7 +1031,7 @@ draw_tui :: (db: *Database, layout: *Layout) { day_idx := (it + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; column_width = layout.columns[L_DAYS_IDX + day_idx].width; task_time := task.times[day_idx]; - total_time = add_int64(total_time, task_time); + total_time = add(total_time, task_time); mvprintw_time(xx y, xx x, task_time, xx column_width); x += column_width; } @@ -1085,7 +1075,7 @@ draw_tui :: (db: *Database, layout: *Layout) { } column_width = layout.columns[L_DAYS_IDX + idx].width; - total_time = add_int64(total_time, daily_total); + total_time = add(total_time, daily_total); mvprintw_time(xx y, xx x, daily_total, xx column_width); x += column_width; @@ -1216,6 +1206,19 @@ main :: () { is_exit_requested := false; for 1..args.count-1 { + + if is_equal_to_any(args[it], "--test", "-t") { + if (load_database(*database, db_file_path) == false) { + print_error("Failed to load database."); + exit(1); + } + start := current_time_monotonic(); + update_total_times(*database); + stop := current_time_monotonic(); + print("Took % ms to update total times for % entries.\n", to_seconds(stop-start), database.tasks.count); + exit(0); + } + if is_equal_to_any(args[it], "--help", "-h") { write_strings( "Usage: ttt [OPTION]... [FILE]...\n", @@ -1645,9 +1648,9 @@ main :: () { case #char "T"; compare_tasks :: (x: Task, y: Task) -> s64 { total_x, total_y: s64; - for x.times { total_x = add_int64(total_x, it); }; - for y.times { total_y = add_int64(total_y, it); }; - return sub_int64(total_x, total_y); + for x.times { total_x = add(total_x, it); }; + for y.times { total_y = add(total_y, it); }; + return sub(total_x, total_y); }; quick_sort(db.tasks, compare_tasks); -- cgit v1.2.3 From 9d14c9ed9f0a1db804d2e7404807d00c66549687 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 18 Jul 2023 01:40:20 +0100 Subject: Cleaning up. --- ttt.jai | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index f3cc372..3f78ecd 100644 --- a/ttt.jai +++ b/ttt.jai @@ -180,14 +180,16 @@ Text_Encoding :: enum u8 #specified { UTF8 :: 2; } -// TODO Provide good description. -number_size :: (number: s64, base: s64 = 10) -> s64 { +// Count digits required to represent number on base. Sign is discarded. +count_digits :: (number: s64, base: s64 = 10) -> s64 { if number == 0 return 1; - return cast(s64)floor(log(cast(float64)abs(number))/log(cast(float64)abs(base))) + 1; // TODO So many casts. + return cast(s64) floor( log(cast(float64)abs(number)) / log(cast(float64)abs(base)) ) + 1; } -// WIP TODO Ues compiler time code to see the auto bake being used... just for fun, once! :D -truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 #no_abc { // TODO Should I use #no_abc ? +// Truncates the string to the length provided or shorter, in case of UTF8 strings that require so. +// Truncation is done by zeroing the tail of the string in place. +// Returns length of truncated string. +truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 { assert(str.data != null, ASSERT_NOT_NULL, "str"); assert(str.count >= length, "'str.count' should be equal or greater to 'length'."); @@ -214,7 +216,7 @@ truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) && !(continuation_bytes == 2 && (data[idx - 1] & 0xF0) == 0xE0) && !(continuation_bytes == 3 && (data[idx - 1] & 0xF8) == 0xF0) ) { - length -= (continuation_bytes + 1); // Remove '+ 1' start byte. + length -= (continuation_bytes + 1); // Remove start byte, ence '+ 1'. } } @@ -285,9 +287,8 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); } else { - // TODO Solve unicode emoji gone wild. - //return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); - //return mvprintw(y, x, "%*s \xE2\x99\xBE %*s", left_padding, "", right_padding, ""); + // TODO Set back the unicode emoji once ncurses has been replaced. + // return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); return mvprintw(y, x, "%*s inf %*s", left_padding, "", right_padding, ""); } } @@ -352,8 +353,7 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us get_msb :: (value: s64) -> msb: s64, found: bool { result: s64 = ---; #asm { - mov val: gpr, value; - bsr result, val; + bsr result, value; } return result * xx cast(bool)value, xx value; // If value is zero: return `0, false`. } @@ -878,7 +878,7 @@ initialize_tui :: () { } // TODO - //setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. + // setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. stdscr = initscr(); // Start curses mode. cbreak(); // Line buffering disabled; pass on everty thing to me. keypad(stdscr, true); // I need those nifty F1..F12. @@ -1047,7 +1047,7 @@ draw_tui :: (db: *Database, layout: *Layout) { /////////////////////////////////////////////////////////////////////////// // Draw selected/total tasks. - size := 1 + number_size(db.selected_idx + 1) + 1 + number_size(db.tasks.count) + 1; // " XXX/YYY " + size := 1 + count_digits(db.selected_idx + 1) + 1 + count_digits(db.tasks.count) + 1; // " XXX/YYY " if (size <= layout.columns[L_TITLE_IDX].width) { mvprintw(size_y - 1, 1, " %td/%zd ", db.selected_idx + 1, db.tasks.count); } -- cgit v1.2.3 From c42b75136be325ddb7eef2705f299f8c4a6218a7 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 28 Jul 2023 00:33:52 +0100 Subject: Prototype implementation of coalesce feature. --- ttt.jai | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 3f78ecd..7701670 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1234,6 +1234,7 @@ main :: () { " 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", @@ -1698,7 +1699,7 @@ main :: () { case #char "w"; #through; case #char "W"; if (db != *database || db.tasks.count <= 0) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to archive duplicates and reset all. ") == true) { // TODO Improve message. + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to archive duplicates and reset all. ") == true) { for db.tasks { if (append_to_csv(it, ar_file_path) == false) { print_error("Failed to archive entry."); // TODO Improve this. @@ -1707,6 +1708,33 @@ main :: () { } trigger_autosave(); } + + // Coalesce similar entries. + case #char "c"; #through; + case #char "C"; + if (db.tasks.count <= 0) continue; + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to coalesce similar tasks. ") == true) { + // TODO Coalesce stuff. + print_error(" REQUIRES CODE CLEANUP AND TESTING "); + tasks_to_process := db.tasks.count - 1; + idx := 0; + while tasks_to_process > 0 { + task := *db.tasks[idx]; + for < i : db.tasks.count-1..idx+1 { + if compare_strings(xx task.name, xx db.tasks[i].name) == 0 { + for item, index: db.tasks[i].times { + task.times[index] = add(task.times[index], item); + } + delete_task(db, i); + tasks_to_process -= 1; + } + } + + idx += 1; + tasks_to_process -= 1; + } + trigger_autosave(); + } case KEY_HOME; select_task(db, 0); -- cgit v1.2.3 From a0a0393e262d23a9718bba030a34d1db540f104a Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 28 Jul 2023 12:58:43 +0100 Subject: Shrink database... just for fun. --- ttt.jai | 42 +++++++++++------------------------------- 1 file changed, 11 insertions(+), 31 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 7701670..aa511ea 100644 --- a/ttt.jai +++ b/ttt.jai @@ -349,32 +349,14 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us active_idx = -1; } - // TODO Helper function. - get_msb :: (value: s64) -> msb: s64, found: bool { - result: s64 = ---; - #asm { - bsr result, value; + // Try to shrink database capacity if using more than 2MB. + 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 result * xx cast(bool)value, xx value; // If value is zero: return `0, false`. - } - - my_array_reserve_nonpoly :: (array: *[..] *void, desired_items: s64, size: s64) -> success: bool { - if !array.allocator.proc remember_allocators(array); - - new_array_data := realloc(array.data, desired_items * size, array.allocated * size, array.allocator); - if new_array_data == null return false; - - array.data = new_array_data; - array.allocated = desired_items; - return true; - } - - // If possible, shrink database capacity. - // TODO Do we really want to make this fuss? - current_capacity := tasks.allocated; - if (tasks.count < (current_capacity >> 2)) { - new_capacity := 1 << (get_msb(tasks.count) + 2); - my_array_reserve_nonpoly(xx *tasks, new_capacity, SIZE_OF_TASK); } return true; @@ -462,7 +444,6 @@ update_total_times :: (db: *Database) { totals: []s64 = db.total_times; memset(totals.data, 0, NUM_WEEK_DAYS * size_of(s64)); for db.tasks { - // TODO Try to use local variables instead of total sub...something... the indexes thingy. times : []s64 = it.times; totals[0] = add(totals[0], times[0]); totals[1] = add(totals[1], times[1]); @@ -592,8 +573,6 @@ load_database :: (db: *Database, path: string) -> success: bool { // Returns success. export_to_csv :: (db: Database, path: string) -> success: bool { assert(xx path, ASSERT_NOT_EMPTY, "path"); - // TODO Make sure (IN ALL PROCEDURES) we're not receiving an empty path. - builder: String_Builder; defer reset(*builder); @@ -608,8 +587,7 @@ export_to_csv :: (db: Database, path: string) -> success: bool { 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]); + name, it.times[0], it.times[1], it.times[2], it.times[3], it.times[4], it.times[5], it.times[6]); } write_entire_file(path, *builder); @@ -762,7 +740,8 @@ append_to_csv :: (task: Task, path: string) -> success: bool { task_name := copy_temporary_string(xx task.name); // TODO Cleanup this temp mess. 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]); + 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]); file_write(*file, csv_line); return true; @@ -1782,3 +1761,4 @@ main :: () { exit(xx ifx error_saving then 1 else 0); } + -- cgit v1.2.3 From f9bbb8fa35f64fe722086c785974e96427c5ab28 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 28 Jul 2023 13:33:58 +0100 Subject: Removed saturating arithmetic test code. --- ttt.jai | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index aa511ea..b40c744 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1185,19 +1185,6 @@ main :: () { is_exit_requested := false; for 1..args.count-1 { - - if is_equal_to_any(args[it], "--test", "-t") { - if (load_database(*database, db_file_path) == false) { - print_error("Failed to load database."); - exit(1); - } - start := current_time_monotonic(); - update_total_times(*database); - stop := current_time_monotonic(); - print("Took % ms to update total times for % entries.\n", to_seconds(stop-start), database.tasks.count); - exit(0); - } - if is_equal_to_any(args[it], "--help", "-h") { write_strings( "Usage: ttt [OPTION]... [FILE]...\n", @@ -1547,7 +1534,7 @@ main :: () { continue; } - if (add_task(db, selected_task) == null) continue; // TODO Show error? + add_task(db, selected_task); trigger_autosave(); -- cgit v1.2.3 From 1473ed9db288f36c736726da62dd96133023a568 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 29 Jul 2023 18:47:48 +0100 Subject: Fixed bug when trying to duplicate entries. --- ttt.jai | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index b40c744..52d22d2 100644 --- a/ttt.jai +++ b/ttt.jai @@ -29,6 +29,12 @@ #load "Integer_Saturating_Arithmetic.jai"; +// TODO List: +// [ ] Decide once and for all if we're calling them entries or tasks. +// [ ] Test shrinking mechanism... this may be done by using the coalescing function. +// [ ] Every time we add or remove entries to the database, it may be reallocated, +// thus making the selected_task and active_task pointers invalid. Check this is not messing up the app. + VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2023"; FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). @@ -307,9 +313,12 @@ is_valid_index :: inline(db: Database, index: s64) -> bool { return index >= 0 & // Adds a task to the database and returns it. // If necessary, expands database capacity. -add_task :: (db: *Database, task: Task = .{}) -> task: *Task, index: s64 { +add_task :: (db: *Database, task: *Task = null) -> task: *Task, index: s64 { assert(db != null, ASSERT_NOT_NULL, "db"); - array_add(*db.tasks, task); + + new_task: Task = .{}; + if task != null new_task = < bool { // TODO Maybe use `us } // Try to shrink database capacity if using more than 2MB. + // TODO WIP WIP WIP if (tasks.allocated >> 2) > tasks.count && tasks.allocated * SIZE_OF_TASK > 2_000_000 { new_capacity := tasks.allocated >> 1; + msg := tprint("shrinking from % to %\n", tasks.allocated, new_capacity); + print_error(msg); new_tasks_data := realloc(tasks.data, new_capacity * SIZE_OF_TASK, tasks.allocated * SIZE_OF_TASK, tasks.allocator); if new_tasks_data != null { + msg = tprint("OH NOEWS"); + print_error(msg); tasks.data = new_tasks_data; tasks.allocated = new_capacity; } + msg = tprint("ALL OK"); + print_error(msg); } return true; @@ -1477,21 +1493,10 @@ main :: () { for 0..input.count-1 { ch := to_lower(input[it]); if ch == { - case #char "m"; - multiplier = xx SECONDS_IN_MINUTE; - break; - - case #char "h"; - multiplier = xx SECONDS_IN_HOUR; - break; - - case #char "d"; - multiplier = xx SECONDS_IN_DAY; - break; - - case #char "y"; - multiplier = xx SECONDS_IN_YEAR; - break; + 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; } } @@ -1534,8 +1539,10 @@ main :: () { continue; } - add_task(db, selected_task); - + if (add_task(db, selected_task) == null) { + print_error("Failed to duplicate entry."); + continue; + } trigger_autosave(); case KEY_F5; -- cgit v1.2.3 From 4fc88c787015268ecb658a72c17749b33200f9b9 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 29 Jul 2023 23:17:53 +0100 Subject: Fixed another bug on add_task. --- ttt.jai | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 52d22d2..a35ac48 100644 --- a/ttt.jai +++ b/ttt.jai @@ -316,12 +316,13 @@ is_valid_index :: inline(db: Database, index: s64) -> bool { return index >= 0 & add_task :: (db: *Database, task: *Task = null) -> task: *Task, index: s64 { assert(db != null, ASSERT_NOT_NULL, "db"); - new_task: Task = .{}; - if task != null new_task = < Date: Mon, 31 Jul 2023 09:06:54 +0100 Subject: WIP : Added notes to fix delete_task. --- ttt.jai | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index a35ac48..b210129 100644 --- a/ttt.jai +++ b/ttt.jai @@ -360,11 +360,14 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us } // Try to shrink database capacity if using more than 2MB. - // TODO WIP WIP WIP + TODO WIP WIP WIP Coalescing is failing because of this... + // if (tasks.allocated >> 2) > tasks.count && tasks.allocated * SIZE_OF_TASK > 2_000_000 { + print("> A <"); new_capacity := tasks.allocated >> 1; msg := tprint("shrinking from % to %\n", tasks.allocated, new_capacity); print_error(msg); + // if !tasks.allocator.proc remember_allocators(*tasks); new_tasks_data := realloc(tasks.data, new_capacity * SIZE_OF_TASK, tasks.allocated * SIZE_OF_TASK, tasks.allocator); if new_tasks_data != null { msg = tprint("OH NOEWS"); @@ -374,6 +377,7 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us } msg = tprint("ALL OK"); print_error(msg); + print("> B <"); } return true; @@ -1695,7 +1699,7 @@ main :: () { while tasks_to_process > 0 { task := *db.tasks[idx]; for < i : db.tasks.count-1..idx+1 { - if compare_strings(xx task.name, xx db.tasks[i].name) == 0 { + if compare_strings(xx task.name, xx db.tasks[i].name) == 0 { for item, index: db.tasks[i].times { task.times[index] = add(task.times[index], item); } -- cgit v1.2.3 From 6a33f413c7e1fd5c6b904d0a01e3f9e1b93bceb0 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 8 Aug 2023 01:25:39 +0100 Subject: Implemented coalesce functionality. --- ttt.jai | 122 +++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 60 insertions(+), 62 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index b210129..53243b9 100644 --- a/ttt.jai +++ b/ttt.jai @@ -360,24 +360,13 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us } // Try to shrink database capacity if using more than 2MB. - TODO WIP WIP WIP Coalescing is failing because of this... - // if (tasks.allocated >> 2) > tasks.count && tasks.allocated * SIZE_OF_TASK > 2_000_000 { - print("> A <"); new_capacity := tasks.allocated >> 1; - msg := tprint("shrinking from % to %\n", tasks.allocated, new_capacity); - print_error(msg); - // if !tasks.allocator.proc remember_allocators(*tasks); new_tasks_data := realloc(tasks.data, new_capacity * SIZE_OF_TASK, tasks.allocated * SIZE_OF_TASK, tasks.allocator); if new_tasks_data != null { - msg = tprint("OH NOEWS"); - print_error(msg); tasks.data = new_tasks_data; tasks.allocated = new_capacity; } - msg = tprint("ALL OK"); - print_error(msg); - print("> B <"); } return true; @@ -421,6 +410,26 @@ move_task :: (db: *Database, source: s64, target: s64) { // TODO Maybe `using db db.selected_idx = target; } +// Find similar task and return it's index, or -1 if not found. +find_similar_task :: (db: *Database, task: Task) -> 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 && 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"); @@ -1616,22 +1625,18 @@ main :: () { case #char "s"; #through; case #char "S"; // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. - active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{}; sort_by := read_input_char(selected_task_row, action_style, " Sort by (n) name, (1..7) day, or (t) total time. "); + sort_procedure: (a: Task, b: Task) -> s64; + 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"; - quick_sort(db.tasks, (x, y) => compare_strings(xx x.name, xx y.name)); + sort_procedure = (x, y) => compare_strings(xx x.name, xx y.name); case #char "t"; #through; case #char "T"; - compare_tasks :: (x: Task, y: Task) -> s64 { - total_x, total_y: s64; - for x.times { total_x = add(total_x, it); }; - for y.times { total_y = add(total_y, it); }; - return sub(total_x, total_y); - }; - quick_sort(db.tasks, compare_tasks); + 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; @@ -1643,33 +1648,22 @@ main :: () { sort_by_idx := sort_by - #char "1"; day := (sort_by_idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; if day == { - case 0; quick_sort(db.tasks, (x, y) => x.times[0] - y.times[0]); - case 1; quick_sort(db.tasks, (x, y) => x.times[1] - y.times[1]); - case 2; quick_sort(db.tasks, (x, y) => x.times[2] - y.times[2]); - case 3; quick_sort(db.tasks, (x, y) => x.times[3] - y.times[3]); - case 4; quick_sort(db.tasks, (x, y) => x.times[4] - y.times[4]); - case 5; quick_sort(db.tasks, (x, y) => x.times[5] - y.times[5]); - case 6; quick_sort(db.tasks, (x, y) => x.times[6] - y.times[6]); + 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 { - - 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 active_task.name, xx it.name) == 0 && compare_array(active_task.times, it.times) == 0 { - db.active_idx = it_index; - break; - } - } + db.active_idx = find_similar_task(db, active_task); } trigger_autosave(); @@ -1691,28 +1685,32 @@ main :: () { case #char "c"; #through; case #char "C"; if (db.tasks.count <= 0) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to coalesce similar tasks. ") == true) { - // TODO Coalesce stuff. - print_error(" REQUIRES CODE CLEANUP AND TESTING "); - tasks_to_process := db.tasks.count - 1; - idx := 0; - while tasks_to_process > 0 { - task := *db.tasks[idx]; - for < i : db.tasks.count-1..idx+1 { - if compare_strings(xx task.name, xx db.tasks[i].name) == 0 { - for item, index: db.tasks[i].times { - task.times[index] = add(task.times[index], item); - } - delete_task(db, i); - tasks_to_process -= 1; + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to coalesce similar tasks. ") == false) continue; + + 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 { + for 0..6 { + t_head.times[it] = add(t_head.times[it], t_tail.times[it]); } + delete_task(db, tail_idx); } - - idx += 1; - tasks_to_process -= 1; + tail_idx -= 1; } - trigger_autosave(); + head_idx += 1; + } + update_total_times(db); // TODO Can we make this so that we don't need to do this? I bet we can... + + if db.active_idx >= 0 { + db.active_idx = find_similar_task(db, active_task); } + trigger_autosave(); case KEY_HOME; select_task(db, 0); -- cgit v1.2.3 From 821ff8453d247d51922fcf11de36b2923b3171bc Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 15 Aug 2023 01:11:30 +0100 Subject: Fixed coalesce feature. --- ttt.jai | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 53243b9..68d3e33 100644 --- a/ttt.jai +++ b/ttt.jai @@ -523,6 +523,20 @@ add_task_time :: (db: *Database, index: s64, day: int, time: s64) { 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: [7] 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"); @@ -1626,6 +1640,8 @@ main :: () { case #char "S"; // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. sort_by := read_input_char(selected_task_row, action_style, " Sort by (n) name, (1..7) day, or (t) total time. "); + show_processing(); + sort_procedure: (a: Task, b: Task) -> s64; active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{}; if sort_by == { @@ -1671,23 +1687,23 @@ main :: () { case #char "w"; #through; case #char "W"; if (db != *database || db.tasks.count <= 0) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to archive duplicates and reset all. ") == true) { - for db.tasks { - if (append_to_csv(it, ar_file_path) == false) { - print_error("Failed to archive entry."); // TODO Improve this. - } - reset_task_times(db, it_index); + if (read_enter_confirmation(selected_task_row, action_style, " Press enter to archive duplicates and reset all. ") == false) continue; + show_processing(); + + for db.tasks { + if (append_to_csv(it, ar_file_path) == false) { + print_error("Failed to archive entry."); // TODO Improve this. } - trigger_autosave(); + reset_task_times(db, it_index); } + trigger_autosave(); // Coalesce similar entries. case #char "c"; #through; case #char "C"; if (db.tasks.count <= 0) continue; if (read_enter_confirmation(selected_task_row, action_style, " Press enter to coalesce similar tasks. ") == false) continue; - - active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{}; + show_processing(); head_idx := 0; while head_idx < db.tasks.count - 1 { @@ -1696,20 +1712,13 @@ main :: () { t_head := *db.tasks[head_idx]; t_tail := *db.tasks[tail_idx]; if compare(xx t_head.name, xx t_tail.name) == 0 { - for 0..6 { - t_head.times[it] = add(t_head.times[it], t_tail.times[it]); - } + add_task_times(db, head_idx, db.tasks[tail_idx].times); delete_task(db, tail_idx); } tail_idx -= 1; } head_idx += 1; } - update_total_times(db); // TODO Can we make this so that we don't need to do this? I bet we can... - - if db.active_idx >= 0 { - db.active_idx = find_similar_task(db, active_task); - } trigger_autosave(); case KEY_HOME; -- cgit v1.2.3 From fa1b8ea54646f1a0f3eadef33e3a660b875cc1ff Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 17 Aug 2023 09:36:02 +0100 Subject: WIP Code cleanup. --- ttt.jai | 147 ++++++++++++++++++++++++++++++++++++++++--------------------- unused.jai | 12 +++++ 2 files changed, 109 insertions(+), 50 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 68d3e33..22c6156 100644 --- a/ttt.jai +++ b/ttt.jai @@ -30,10 +30,7 @@ // TODO List: -// [ ] Decide once and for all if we're calling them entries or tasks. -// [ ] Test shrinking mechanism... this may be done by using the coalescing function. -// [ ] Every time we add or remove entries to the database, it may be reallocated, -// thus making the selected_task and active_task pointers invalid. Check this is not messing up the app. +// [ ] Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2023"; @@ -41,7 +38,7 @@ FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). NUM_WEEK_DAYS :: 7; // TODO This has to go - Just to be more clear about what we're looping about. NAME_SIZE :: 72; // TODO Use this instead of Task.name.count ? -APP_FOLDER_NAME :: ".task_time_tracker_test"; // TODO Using _v2 to avoid erasing my work data. +APP_FOLDER_NAME :: ".task_time_tracker_test"; // TODO Using different folder to avoid erasing my work data. DB_FILE_NAME :: "database.bin"; AR_FILE_NAME :: "archive.csv"; DB_FILE_SIGN_STR :: "TTT:B:02"; @@ -70,17 +67,6 @@ Database :: struct { tasks : [..] Task; } -SIZE_OF_TASK :: #run size_of(Task); -// const char DB_FILE_SIGN[] = DB_FILE_SIGN_STR; -// const size_t DB_FILE_SIGN_LENGTH = sizeof(DB_FILE_SIGN_STR)-1; -// const size_t SIZEOF_TASK_ST = sizeof(task_st); -// const size_t SIZEOF_DATABASE_ST = sizeof(database_st); -// const size_t SIZEOF_CHAR = sizeof(char); -// const size_t SIZEOF_INT64 = sizeof(int64_t); -// -// -// - database : Database; archive : Database; is_autosave_enabled := true; @@ -88,8 +74,7 @@ countdown_to_autosave := -1; app_directory : string; db_file_path : string; ar_file_path : string; -// char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors. -// size_t string_buffer_size = 0; + size_x : s32; size_y : s32; pos_x : s32; @@ -176,22 +161,17 @@ is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool return to_compare == test_a || to_compare == test_b; } -// Given an UTF8 encoded string, truncate it to length bytes without breaking any UTF8 character. -// The string should have capacity for at least length + 1. -// The terminating null byte ('\0') is not included in length. -// Returns the truncated string length. +// Count digits required to represent number on base. Sign is discarded. +count_digits :: (number: s64, base: s64 = 10) -> s64 { + if number == 0 return 1; + return cast(s64) floor( log(cast(float64)abs(number)) / log(cast(float64)abs(base)) ) + 1; +} Text_Encoding :: enum u8 #specified { ASCII :: 1; UTF8 :: 2; } -// Count digits required to represent number on base. Sign is discarded. -count_digits :: (number: s64, base: s64 = 10) -> s64 { - if number == 0 return 1; - return cast(s64) floor( log(cast(float64)abs(number)) / log(cast(float64)abs(base)) ) + 1; -} - // Truncates the string to the length provided or shorter, in case of UTF8 strings that require so. // Truncation is done by zeroing the tail of the string in place. // Returns length of truncated string. @@ -360,9 +340,10 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us } // Try to shrink database capacity if using more than 2MB. - if (tasks.allocated >> 2) > tasks.count && tasks.allocated * SIZE_OF_TASK > 2_000_000 { + 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); + 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; @@ -467,22 +448,88 @@ update_times :: (db: *Database) { } } +x_count: s64 = 0; +x_average: float64 = 0.0; + + // Recalculates database totals. update_total_times :: (db: *Database) { assert(db != null, ASSERT_NOT_NULL, "db"); + // print(">>>xpto: %<<<\n", totals); + // print(">>>sizeA: %<<<\n", size_of([7] s64)); + // print(">>>sizeB: %<<<\n", NUM_WEEK_DAYS * size_of(s64)); + // print(">>>sizeC: %<<<\n", size_of(type_of(db.total_times))); + // Initialize(*totals.data); + // print(">>>xpto: %<<<\n", totals); + // totals.data = .[]; + + // TODO WIP + Check what size there is for a [..] s64... + + print(">>%<<", db.total_times.count); + // memset(totals.data, 0, db.total_times.count * size_of(s64)); + // memset(db.total_times.data, 0, size_of(type_of(db.total_times))); + + for xpto: 1..20 { + + start := current_time_monotonic(); + + for * total : db.total_times { < string { + builder: String_Builder; + defer reset(*builder); + for 0..6 { + print_to_builder(*builder, code, it); + } + return builder_to_string(*builder); + } + totals: []s64 = db.total_times; - memset(totals.data, 0, NUM_WEEK_DAYS * size_of(s64)); for db.tasks { times : []s64 = it.times; - totals[0] = add(totals[0], times[0]); - totals[1] = add(totals[1], times[1]); - totals[2] = add(totals[2], times[2]); - totals[3] = add(totals[3], times[3]); - totals[4] = add(totals[4], times[4]); - totals[5] = add(totals[5], times[5]); - totals[6] = add(totals[6], times[6]); + #insert #run unroll_week("totals[%1] = add(totals[%1], times[%1]);"); } + + + + stop := current_time_monotonic(); + average: float64 = xx to_microseconds(stop-start); + x_average = (average + x_count * x_average) / (x_count + 1); + x_count += 1; + } + + print("Measured % us.\n", x_average); } // Resets the times of the provided task (and adjusts database totals). @@ -559,7 +606,7 @@ store_database :: (db: Database, path: string) -> success: bool { 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); + file_write(*file, db.tasks.data, size_of(Task) * db.tasks.count); return true; } @@ -602,7 +649,7 @@ load_database :: (db: *Database, path: string) -> success: bool { array_reserve(*db.tasks, tasks_count); // Read database tasks. - file_read(file, db.tasks.data, SIZE_OF_TASK * tasks_count); + file_read(file, db.tasks.data, size_of(Task) * tasks_count); db.tasks.count = tasks_count; // Make sure we are reading all the file. @@ -1279,9 +1326,9 @@ main :: () { 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 entries are stored in binary format on the 'database.bin' file.\n", + " 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 tasks such as saving to file or recalculating totals times,\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); @@ -1435,7 +1482,7 @@ main :: () { case #char "n"; #through; case #char "N"; if is_database_full(db) { - read_enter_confirmation(selected_task_row, error_style, " Unable to create entry: database is full. "); + read_enter_confirmation(selected_task_row, error_style, " Unable to create task: database is full. "); continue; } @@ -1563,12 +1610,12 @@ main :: () { if selected_task == null continue; if is_database_full(db) { - read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate entry: database is full. "); + read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate task: database is full. "); continue; } if (add_task(db, selected_task) == null) { - print_error("Failed to duplicate entry."); + print_error("Failed to duplicate task."); continue; } trigger_autosave(); @@ -1612,7 +1659,7 @@ main :: () { if (db != *database || selected_task == null || selected_task == active_task) continue; if (append_to_csv(selected_task, ar_file_path) == false) { - print_error("Failed to archive entry."); + print_error("Failed to archive task."); continue; } delete_task(db, db.selected_idx); @@ -1624,12 +1671,12 @@ main :: () { if (db != *archive || selected_task == null) continue; if is_database_full(*database) { - read_enter_confirmation(selected_task_row, error_style, " Unable to restore entry: database is full. "); + read_enter_confirmation(selected_task_row, error_style, " Unable to restore task: database is full. "); continue; } if (add_task(*database, selected_task) == null) { - print_error("Failed to restore entry."); + print_error("Failed to restore task."); continue; } delete_task(db, db.selected_idx); @@ -1692,13 +1739,13 @@ main :: () { for db.tasks { if (append_to_csv(it, ar_file_path) == false) { - print_error("Failed to archive entry."); // TODO Improve this. + print_error("Failed to archive task."); // TODO Improve this. } reset_task_times(db, it_index); } trigger_autosave(); - // Coalesce similar entries. + // Coalesce similar tasks. case #char "c"; #through; case #char "C"; if (db.tasks.count <= 0) continue; diff --git a/unused.jai b/unused.jai index 97d9f88..9e05904 100644 --- a/unused.jai +++ b/unused.jai @@ -1,5 +1,17 @@ auto_release_temp(); // TODO Needs to be tested. print(">%<", S64_MIN + delta, to_standard_error = true); + +// MEASURE PERFORMANCE +{ + #import "Basic"; + start := current_time_monotonic(); + // Code to measure. + stop := current_time_monotonic(); + print("Measured % ns.\n", to_nanoseconds(stop-start)); + + // Cumulative average: + CA_n+1 = (x_n+1 + n*CA_n ) / (n + 1) +} // TODO DEBUG print_owner_allocator :: (tag: string, memory: *void) { -- cgit v1.2.3 From cb5b475a28a9ef5ecbdfc317814464cbf1cf09c1 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 19 Aug 2023 01:48:00 +0100 Subject: Testing performance of different loop approaches. --- ttt.jai | 90 +++++++++++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 28 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 22c6156..fe2655a 100644 --- a/ttt.jai +++ b/ttt.jai @@ -465,27 +465,22 @@ update_total_times :: (db: *Database) { // totals.data = .[]; // TODO WIP - Check what size there is for a [..] s64... + // Check what size there is for a [..] s64... print(">>%<<", db.total_times.count); // memset(totals.data, 0, db.total_times.count * size_of(s64)); // memset(db.total_times.data, 0, size_of(type_of(db.total_times))); - for xpto: 1..20 { + for xpto: 1..50 { start := current_time_monotonic(); for * total : db.total_times { < string { - builder: String_Builder; - defer reset(*builder); - for 0..6 { - print_to_builder(*builder, code, it); - } - return builder_to_string(*builder); - } - - totals: []s64 = db.total_times; - for db.tasks { - times : []s64 = it.times; - #insert #run unroll_week("totals[%1] = add(totals[%1], times[%1]);"); - } + // unroll_week :: (code: string) -> string { + // builder: String_Builder; + // defer reset(*builder); + // for 0..6 { + // print_to_builder(*builder, code, it); + // } + // return builder_to_string(*builder); + // } +// + // totals: []s64 = db.total_times; + // for db.tasks { + // times : []s64 = it.times; + // #insert #run unroll_week("totals[%1] = add(totals[%1], times[%1]);"); + // } stop := current_time_monotonic(); - average: float64 = xx to_microseconds(stop-start); + average: float64 = xx to_nanoseconds(stop-start); x_average = (average + x_count * x_average) / (x_count + 1); x_count += 1; } - print("Measured % us.\n", x_average); + print("Measured % ns/entry.\n", x_average/(cast(float64)db.tasks.count)); } // Resets the times of the provided task (and adjusts database totals). -- cgit v1.2.3 From 1c6b84fb34a0dd18020f153b2d6cbd5e4f741319 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 19 Aug 2023 02:15:34 +0100 Subject: Testing performance. --- ttt.jai | 96 +++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 49 insertions(+), 47 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index fe2655a..22a0f42 100644 --- a/ttt.jai +++ b/ttt.jai @@ -471,32 +471,32 @@ update_total_times :: (db: *Database) { // memset(totals.data, 0, db.total_times.count * size_of(s64)); // memset(db.total_times.data, 0, size_of(type_of(db.total_times))); - for xpto: 1..50 { + for xpto: 1..100 { start := current_time_monotonic(); for * total : db.total_times { < string { // builder: String_Builder; -- cgit v1.2.3 From f8080876d29be1241a59ef96c3ddb00bef84b60a Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 19 Aug 2023 02:44:17 +0100 Subject: Performance analysis on loop unrolling. --- ttt.jai | 77 +++++++++++++++++++++++++---------------------------------------- 1 file changed, 30 insertions(+), 47 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 22a0f42..15b5a66 100644 --- a/ttt.jai +++ b/ttt.jai @@ -481,26 +481,26 @@ update_total_times :: (db: *Database) { // LLVM 14.5 5.0 // X64 132.4 124.4 // - totals: []s64 = db.total_times; - for db.tasks { - times : []s64 = it.times; - totals[0] = add(totals[0], times[0]); - totals[1] = add(totals[1], times[1]); - totals[2] = add(totals[2], times[2]); - totals[3] = add(totals[3], times[3]); - totals[4] = add(totals[4], times[4]); - totals[5] = add(totals[5], times[5]); - totals[6] = add(totals[6], times[6]); - } + // totals: []s64 = db.total_times; + // for db.tasks { + // times : []s64 = it.times; + // totals[0] = add(totals[0], times[0]); + // totals[1] = add(totals[1], times[1]); + // totals[2] = add(totals[2], times[2]); + // totals[3] = add(totals[3], times[3]); + // totals[4] = add(totals[4], times[4]); + // totals[5] = add(totals[5], times[5]); + // totals[6] = add(totals[6], times[6]); + // } // I5-2550K R3550H (ns/entry) // LLVM 11.2 11.7 - // X64 119.9 124.5 + // X64 118.2 124.5 // - for db.tasks { - for *total: db.total_times { - < string { - // builder: String_Builder; - // defer reset(*builder); - // for 0..6 { - // print_to_builder(*builder, code, it); - // } - // return builder_to_string(*builder); - // } -// // totals: []s64 = db.total_times; // for db.tasks { // times : []s64 = it.times; - // #insert #run unroll_week("totals[%1] = add(totals[%1], times[%1]);"); + // for 0..totals.count-1 { + // totals[it] = add(totals[it], times[it]); + // } // } - stop := current_time_monotonic(); -- cgit v1.2.3 From c87ae97113883b39e2c05958d6a79891bec44bc4 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 19 Aug 2023 02:48:30 +0100 Subject: Selected implementation for update_total_times. --- ttt.jai | 92 +---------------------------------------------------------------- 1 file changed, 1 insertion(+), 91 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 15b5a66..f51fdb5 100644 --- a/ttt.jai +++ b/ttt.jai @@ -448,107 +448,17 @@ update_times :: (db: *Database) { } } -x_count: s64 = 0; -x_average: float64 = 0.0; - - // Recalculates database totals. update_total_times :: (db: *Database) { assert(db != null, ASSERT_NOT_NULL, "db"); - // print(">>>xpto: %<<<\n", totals); - // print(">>>sizeA: %<<<\n", size_of([7] s64)); - // print(">>>sizeB: %<<<\n", NUM_WEEK_DAYS * size_of(s64)); - // print(">>>sizeC: %<<<\n", size_of(type_of(db.total_times))); - // Initialize(*totals.data); - // print(">>>xpto: %<<<\n", totals); - // totals.data = .[]; - - // TODO WIP - // Check what size there is for a [..] s64... - - print(">>%<<", db.total_times.count); - // memset(totals.data, 0, db.total_times.count * size_of(s64)); - // memset(db.total_times.data, 0, size_of(type_of(db.total_times))); - - for xpto: 1..100 { + for *total: db.total_times { < Date: Thu, 24 Aug 2023 09:15:44 +0100 Subject: Replacing ncurses with tio. --- tio.jai | 116 ++++++++++++++++++++++++++++++++++ ttt.jai | 215 ++++++++++++++++++++++++++++++++++++---------------------------- 2 files changed, 237 insertions(+), 94 deletions(-) create mode 100644 tio.jai (limited to 'ttt.jai') diff --git a/tio.jai b/tio.jai new file mode 100644 index 0000000..37c7e4a --- /dev/null +++ b/tio.jai @@ -0,0 +1,116 @@ + +terminal_state : struct { + // size : ivec2; + width: s32; + height: s32; + // cursor := ivec2.{-1, -1}; // {-1, -1} if cursor hidden + x: s32 = -1; + y: s32 = -1; + last_mode : Graphics_Mode; +} + +#if OS == .LINUX { + #import "POSIX"; + + __term : My_Termios; + + My_Termios :: struct { + c_iflag : u32; + c_oflag : u32; + c_cflag : u32; + c_lflag : u32; + unknown_pad : u8; + c_cc : [32]u8; + c_ispeed : u32; + c_ospeed : u32; + } + + libc :: #system_library "libc"; + tcsetattr :: (fd : s32, optional_actions : s32, termios_p : *My_Termios) -> s32 #foreign libc; + tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; +} +else { + assert(false, "unsupported OS\n"); +} + +Graphics_Mode :: struct { + foreground : Color; + background : Color; + attr_flags : enum_flags u8 { + F_BOLD :: 0x1; + F_DIM :: 0x2; + F_ITALIC :: 0x4; + F_UNDERLINE :: 0x8; + F_BLINKING :: 0x10; + F_INVERSE :: 0x20; + F_STRIKETHROUGH :: 0x40; + } + // attrs : [MAX_ATTRS]bool; + // 0 - bold (on/off/keep) + // 1 - dim/faint + // 2 - italic + // 3 - underline + // 4 - blinking + // 5 - inverse + // 6 - strikethrough + fcol256 : u8; + bcol256 : u8; +} + +Color :: enum u8 { + RESET :: 0; + DEFAULT :: 39; + COLOR256 :: 38; + + BLACK :: 30; + RED :: 31; + GREEN :: 32; + YELLOW :: 33; + BLUE :: 34; + MAGENTA :: 35; + CYAN :: 36; + WHITE :: 37; + + BRIGHT_BLACK :: 90; + BRIGHT_RED :: 91; + BRIGHT_GREEN :: 92; + BRIGHT_YELLOW :: 93; + BRIGHT_BLUE :: 94; + BRIGHT_MAGENTA :: 95; + BRIGHT_CYAN :: 96; + BRIGHT_WHITE :: 97; +} + +__old_logger : type_of(context.logger); + +initialize :: () { + #if OS == .LINUX { + tcgetattr(STDIN_FILENO, *__term); + } else { + assert(false, "procedure call on unsupported OS\n"); + } +} + +terminate :: () { + #if OS == .LINUX { + tcsetattr(STDIN_FILENO, 0, *__term); // return echo + + tio_write("\e[?47l"); // restore screen + tio_write("\e8"); // restore cursor + tio_write("\e[?25h"); // show cursor + tio_write("\e[?30h"); // show scrollbar + terminal_state = .{}; + + context.logger = __old_logger; + } else { + assert(false, "procedure call on unsupported OS\n"); + } +} + +tio_write :: (str : string) { + #if OS == .LINUX { + write(STDIN_FILENO, str.data, xx str.count); + } else { + assert(false, "procedure call on unsupported OS\n"); + } +} diff --git a/ttt.jai b/ttt.jai index f51fdb5..b3a1cd3 100644 --- a/ttt.jai +++ b/ttt.jai @@ -25,12 +25,31 @@ #import "File"; #import "File_Utilities"; #import "String"; -#import "curses"; +//#import "curses"; +// #import "kscurses"; +TIO :: #import "tio"; #load "Integer_Saturating_Arithmetic.jai"; // TODO List: // [ ] Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. +stdscr: *void; // TODO DAM +A_BOLD: s32 = 0; // TODO DAM +COLOR_PAIR :: (a: s32) -> s32 { return 0; } // TODO DAM +ERR :: -1; // TODO DAM +KEY_RESIZE :: 410; // TODO DAM +KEY_F2 :: 266; // TODO DAM +KEY_F5 :: 269; // TODO DAM +KEY_HOME :: 262; // TODO DAM +KEY_UP :: 259; // TODO DAM +KEY_DOWN :: 258; // TODO DAM +KEY_NPAGE :: 338; // TODO DAM +KEY_PPAGE :: 339; // TODO DAM +KEY_END :: 360; // TODO DAM +KEY_BACKSPACE :: 263; // TODO DAM +KEY_DC :: 330; // TODO DAM +WINDOW :: struct { } // TODO DAM + VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2023"; @@ -97,7 +116,7 @@ error_window : *WINDOW = null; error_time_limit := Apollo_Time.{0, 0}; print_error :: (format :string, args : .. Any) { - if stdscr == null || isendwin() == true { // Not in ncurses mode? + if stdscr == null { // || isendwin() == true { // Not in ncurses mode? TODO DAM print(format, ..args); print("\n"); } @@ -106,16 +125,16 @@ print_error :: (format :string, args : .. Any) { w_size_x: s32 = ifx size_x > 120 then 120 else size_x - 2; w_size_y: s32 = 4; if (error_window == null) { - error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); - wattron(error_window, COLOR_PAIR(xx Styles.ERROR)); - wborder(error_window, CHAR_SPACE, CHAR_SPACE, 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); - mvwprintw(error_window, 0, 1, " Error "); - wmove(error_window, 1, 0); + //error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); TODO DAM + //wattron(error_window, COLOR_PAIR(xx Styles.ERROR)); TODO DAM + //wborder(error_window, CHAR_SPACE, CHAR_SPACE, 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); TODO DAM + //mvwprintw(error_window, 0, 1, " Error "); TODO DAM + //wmove(error_window, 1, 0); TODO DAM } else { - waddch(error_window, CHAR_SPACE); + //waddch(error_window, CHAR_SPACE); TODO DAM } - wprintw(error_window, temp_c_string(tprint(format, args))); + //wprintw(error_window, temp_c_string(tprint(format, args))); TODO DAM error_time_limit = current_time_monotonic() + seconds_to_apollo(5); } } @@ -126,12 +145,12 @@ draw_error_window :: () { // Hide error window after time-limit or if terminal is shrank. w_size_x: s32; w_size_y: s32; - getmaxyx(error_window, *w_size_y, *w_size_x); + //getmaxyx(error_window, *w_size_y, *w_size_x); TODO DAM if (current_time_monotonic() >= error_time_limit || size_x - w_size_x < 2 || size_y - w_size_y < 2 ) { - delwin(error_window); + //delwin(error_window); TODO DAM error_window = null; return; } @@ -139,12 +158,12 @@ draw_error_window :: () { // Adjust error window position. pos_x := (size_x - w_size_x) / 2; pos_y := (size_y - w_size_y) / 2; - mvwin(error_window, pos_y, pos_x); + //mvwin(error_window, pos_y, pos_x); TODO DAM // Avoid being overwritten by main window content. - refresh(); - touchwin(error_window); - wrefresh(error_window); + //refresh(); TODO DAM + //touchwin(error_window); TODO DAM + //wrefresh(error_window); TODO DAM } trigger_autosave :: () { @@ -152,8 +171,8 @@ trigger_autosave :: () { } show_processing :: () { - mvaddch(0, 0, ACS_DIAMOND); - refresh(); + //mvaddch(0, 0, ACS_DIAMOND); TODO DAM + //refresh(); TODO DAM } // Returns true if string to_compare is equal to any of the other passed strings, false otherwise. @@ -243,18 +262,22 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { right_padding := space - TIME_CHARS - left_padding; if time < 0 { - return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, ""); TODO DAM } else if time == 0 { - return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, ""); TODO DAM } else if time < SECONDS_IN_MINUTE { - return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); TODO DAM } 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; - return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); TODO DAM } else if time < #run mul_f64_s64(9999.5, SECONDS_IN_DAY) { value := cast(float64) time / SECONDS_IN_DAY; @@ -262,7 +285,8 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { 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; - return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); TODO DAM } else if time < #run mul_f64_s64(9999.5, SECONDS_IN_YEAR) { value := cast(float64) time / SECONDS_IN_YEAR; @@ -270,12 +294,13 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { 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; - return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); TODO DAM } else { // TODO Set back the unicode emoji once ncurses has been replaced. - // return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); - return mvprintw(y, x, "%*s inf %*s", left_padding, "", right_padding, ""); + return 0; + // return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); TODO DAM } } @@ -876,22 +901,23 @@ initialize_tui :: () { } } - // TODO - // setlocale(LC_ALL, "C.UTF-8"); // Sets locale for C library functions; Allows usage of UTF-8. - stdscr = initscr(); // Start curses mode. - cbreak(); // Line buffering disabled; pass on everty thing to me. - keypad(stdscr, true); // I need those nifty F1..F12. - curs_set(0); // Set cursor invisible. - noecho(); // Disable echoing input characters. + // TODO DAM + TIO.initialize(); + TIO.terminate(); // TODO DAM + //stdscr = initscr(); // Start curses mode. TODO DAM + //cbreak(); // Line buffering disabled; pass on everty thing to me. TODO DAM + //keypad(stdscr, true); // I need those nifty F1..F12. TODO DAM + //curs_set(0); // Set cursor invisible. TODO DAM + //noecho(); // Disable echoing input characters. TODO DAM // Initialize pairs of colors. - start_color(); - use_default_colors(); // Using default (-1) instead of COLOR_BLACK. - init_pair(xx Styles.SELECTED, COLOR_BLACK, COLOR_CYAN); - init_pair(xx Styles.SELECTED_INVERTED, COLOR_CYAN, -1); - init_pair(xx Styles.ACTIVE, COLOR_BLUE, -1); - init_pair(xx Styles.ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); - init_pair(xx Styles.ERROR, COLOR_RED, -1); + //start_color(); TODO DAM + //use_default_colors(); // Using default (-1) instead of COLOR_BLACK. TODO DAM + //init_pair(xx Styles.SELECTED, COLOR_BLACK, COLOR_CYAN); TODO DAM + //init_pair(xx Styles.SELECTED_INVERTED, COLOR_CYAN, -1); TODO DAM + //init_pair(xx Styles.ACTIVE, COLOR_BLUE, -1); TODO DAM + //init_pair(xx Styles.ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); TODO DAM + //init_pair(xx Styles.ERROR, COLOR_RED, -1); TODO DAM } update_layout :: () { @@ -930,11 +956,11 @@ draw_tui :: (db: *Database, layout: *Layout) { now_week_day := to_calendar(now_utc, .LOCAL).day_of_week_starting_at_0; // Reset theme and clear screen. - attrset(A_NORMAL); - erase(); + //attrset(A_NORMAL); TODO DAM + //erase(); TODO DAM // Draw outer border. - box(stdscr, 0, 0); + //box(stdscr, 0, 0); TODO DAM // Draw table grids. // TODO Maybe this could be simplified? @@ -943,11 +969,11 @@ draw_tui :: (db: *Database, layout: *Layout) { for 0..layout.columns.count-2 { column := layout.columns[it]; x += 1 + column.width; - mvaddch(xx y, xx x, ACS_TTEE); + //mvaddch(xx y, xx x, ACS_TTEE); TODO DAM for row: 1..size_y-1 { - mvaddch(xx row, xx x, ACS_VLINE); + //mvaddch(xx row, xx x, ACS_VLINE); TODO DAM } - mvaddch(size_y-1, xx x, ACS_BTEE); + //mvaddch(size_y-1, xx x, ACS_BTEE); TODO DAM } @@ -959,7 +985,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Headers : title x += 1; col = *layout.columns[L_TITLE_IDX]; - mvaddstr(xx y, xx (x + col.alignment_offset), ifx db == *archive then layout.archive_title.data else col.header.data); + //mvaddstr(xx y, xx (x + col.alignment_offset), ifx db == *archive then layout.archive_title.data else col.header.data); TODO DAM x += col.width; // Headers : days @@ -969,24 +995,24 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (idx == now_week_day && active_task != null) { - attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); + // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM } else if (idx == now_week_day) { - attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); + // attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); TODO DAM } col = *layout.columns[L_DAYS_IDX + idx]; - mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); + //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM x += col.width; // Reset theme. - attrset(A_NORMAL); + //attrset(A_NORMAL); TODO DAM } // Headers : total x += 1; col = *layout.columns[L_TOTAL_IDX]; - mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); + //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM /////////////////////////////////////////////////////////////////////////// @@ -1008,19 +1034,19 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (task == active_task && task == selected_task) { - attron(COLOR_PAIR(xx Styles.ACTIVE_SELECTED) | A_BOLD); + // attron(COLOR_PAIR(xx Styles.ACTIVE_SELECTED) | A_BOLD); TODO DAM } else if (task == selected_task) { - attron(COLOR_PAIR(xx Styles.SELECTED)); + // attron(COLOR_PAIR(xx Styles.SELECTED)); TODO DAM } else if (task == active_task) { - attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); + // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM } // Task title. x += 1; column_width = layout.columns[L_TITLE_IDX].width; - mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. + // mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. TODO DAM x += column_width; // Task times. @@ -1040,7 +1066,7 @@ draw_tui :: (db: *Database, layout: *Layout) { mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); // Reset theme. - attrset(A_NORMAL); + //attrset(A_NORMAL); TODO DAM } @@ -1048,10 +1074,10 @@ draw_tui :: (db: *Database, layout: *Layout) { // Draw selected/total tasks. size := 1 + count_digits(db.selected_idx + 1) + 1 + count_digits(db.tasks.count) + 1; // " XXX/YYY " if (size <= layout.columns[L_TITLE_IDX].width) { - mvprintw(size_y - 1, 1, " %td/%zd ", db.selected_idx + 1, db.tasks.count); + // mvprintw(size_y - 1, 1, " %td/%zd ", db.selected_idx + 1, db.tasks.count); TODO DAM } else { - mvprintw(size_y - 1, 1, "%td", db.selected_idx + 1); + // mvprintw(size_y - 1, 1, "%td", db.selected_idx + 1); TODO DAM } @@ -1067,10 +1093,10 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (idx == now_week_day && active_task != null) { - attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); + // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM } else if (idx == now_week_day) { - attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); + // attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); TODO DAM } column_width = layout.columns[L_DAYS_IDX + idx].width; @@ -1079,7 +1105,7 @@ draw_tui :: (db: *Database, layout: *Layout) { x += column_width; // Reset theme. - attrset(A_NORMAL); + //attrset(A_NORMAL); TODO DAM } x += 1; mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); @@ -1098,15 +1124,15 @@ free_memory :: () { read_input_string_padded :: (row: int, column: int, style: s32, length: int, padding: int) -> string { str := talloc_string(length); memset(str.data, 0, str.count); - attron(style | A_UNDERLINE); - mvprintw(xx row, xx column, "%*s", padding, ""); - echo(); - curs_set(1); - mvgetnstr(xx row, xx column, str.data, xx length); + // attron(style | A_UNDERLINE); TODO DAM + // mvprintw(xx row, xx column, "%*s", padding, ""); TODO DAM + //echo(); TODO DAM + //curs_set(1); TODO DAM + //mvgetnstr(xx row, xx column, str.data, xx length); TODO DAM truncate_string(str, length); - noecho(); - curs_set(0); - attrset(A_NORMAL); + //noecho(); TODO DAM + //curs_set(0); TODO DAM + //attrset(A_NORMAL); TODO DAM return str; } @@ -1116,14 +1142,14 @@ read_input_string :: (row: int, column: int, style: s32, length: int) -> string // Returns success. read_input_int :: (row: int, style: s32, message: string) -> value: int, success: bool { - attron(xx style); - move(xx row, 1); - addch(ACS_CKBOARD); - addstr(message.data); // TODO Convert to C type - attrset(A_NORMAL); + // attron(xx style); TODO DAM + //move(xx row, 1); TODO DAM + //addch(ACS_CKBOARD); TODO DAM + //addstr(message.data); // TODO Convert to C type TODO DAM + //attrset(A_NORMAL); TODO DAM // Get line number. - input_pos_x := getcurx(stdscr); + input_pos_x := 0;//getcurx(stdscr); TODO DAM input_width := size_x - input_pos_x - 1; str := read_input_string(row, input_pos_x, style, input_width); @@ -1135,16 +1161,17 @@ read_input_int :: (row: int, style: s32, message: string) -> value: int, success read_input_char :: (row: int, style: int, message: string) -> s32 { assert(message.data != null, ASSERT_NOT_NULL, "message"); // TODO Improve this check? - attron(xx style); - move(xx row, 1); + // attron(xx style); TODO DAM + //move(xx row, 1); TODO DAM for 0..size_x-3 { //for (int idx = 0; idx < size_x - 2; idx++) { // TODO check what's going on here. - addch(ACS_CKBOARD); + //addch(ACS_CKBOARD); TODO DAM } - mvaddstr(xx row, 2, message.data); - attrset(A_NORMAL); + //mvaddstr(xx row, 2, message.data); TODO DAM + //attrset(A_NORMAL); TODO DAM - return getch(); + //return getch(); TODO DAM + return 0; } // Retuns true if user presses enter, false otherwise. @@ -1334,13 +1361,13 @@ main :: () { db := *database; layout := *layouts[Layouts.COMPACT]; - flushinp(); - ungetch(KEY_RESIZE); + //flushinp(); TODO DAM + //ungetch(KEY_RESIZE); TODO DAM while (true) { if (is_terminal_too_small) { INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; - mvaddstr(size_y / 2, (size_x - xx INVALID_WINDOW_MESSAGE.count) / 2, INVALID_WINDOW_MESSAGE); + //mvaddstr(size_y / 2, (size_x - xx INVALID_WINDOW_MESSAGE.count) / 2, INVALID_WINDOW_MESSAGE); TODO DAM } else { draw_tui(db, layout); @@ -1348,11 +1375,11 @@ main :: () { } reset_temporary_storage(); - timeout(INPUT_TIMEOUT_MS); - key := getch(); + //timeout(INPUT_TIMEOUT_MS); TODO DAM + key := 0; // getch(); TODO DAM if key == #char "q" || key == #char "Q" break; update_times(*database); - timeout(INPUT_AWAIT_INF); + //timeout(INPUT_AWAIT_INF); TODO DAM // TODO WIP Remove `selected_task` and `active_task` and helper functions. selected_task := get_selected_task(db); @@ -1387,8 +1414,8 @@ main :: () { // When terminal is resized. case KEY_RESIZE; - clear(); - getmaxyx(stdscr, *size_y, *size_x); + //clear(); TODO DAM + //getmaxyx(stdscr, *size_y, *size_x); TODO DAM is_terminal_too_small = size_x < 60 || size_y < 3; update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; @@ -1428,8 +1455,8 @@ main :: () { trigger_autosave(); // Force rename action. - flushinp(); - ungetch(KEY_F(2)); + //flushinp(); TODO DAM + //ungetch(KEY_F(2)); TODO DAM case KEY_F2; if (selected_task == null) continue; @@ -1735,11 +1762,11 @@ main :: () { if (error_saving) { print_error("Press any key to close."); draw_error_window(); - timeout(INPUT_AWAIT_INF); - getch(); + //timeout(INPUT_AWAIT_INF); TODO DAM + //getch(); TODO DAM } - endwin(); + //endwin(); TODO DAM exit(xx ifx error_saving then 1 else 0); } -- cgit v1.2.3 From 76f3be800b9306a0a63c9f888b5e61c27ea35c0e Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 29 Aug 2023 09:21:24 +0100 Subject: Attempting to replace ncurses. --- tio.jai | 142 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- ttt.jai | 40 ++++++++++-------- 2 files changed, 148 insertions(+), 34 deletions(-) (limited to 'ttt.jai') diff --git a/tio.jai b/tio.jai index 37c7e4a..f6ee0cb 100644 --- a/tio.jai +++ b/tio.jai @@ -1,4 +1,10 @@ +#module_parameters(ENABLE_SIGINT := true, ENABLE_SIGQUIT := true); + +#import "Basic"; + +// TODO Eventually, I'll need to add #scope_module ... + terminal_state : struct { // size : ivec2; width: s32; @@ -15,18 +21,22 @@ terminal_state : struct { __term : My_Termios; My_Termios :: struct { - c_iflag : u32; - c_oflag : u32; - c_cflag : u32; - c_lflag : u32; - unknown_pad : u8; - c_cc : [32]u8; - c_ispeed : u32; - c_ospeed : u32; + c_iflag : u32; // Input mode flags. + c_oflag : u32; // Output mode flags. + c_cflag : u32; // Control mode flags. + c_lflag : u32; // Local mode flags. + c_line : u8; // Line discipline. + c_cc : [32]u8; // Control characters. + c_ispeed : u32; // Input speed. + c_ospeed : u32; // Output speed. } 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; + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; } else { @@ -57,6 +67,52 @@ Graphics_Mode :: struct { bcol256 : u8; } +Key :: enum u64 { + READ_ERROR :: 0xffffffff_ffffffff; + + UP :: 0x41_5b1b; + DOWN :: 0x42_5b1b; + RIGHT :: 0x43_5b1b; + LEFT :: 0x44_5b1b; + + CTRL_UP :: 0x4135_3b315b1b; + CTRL_DOWN :: 0x4235_3b315b1b; + CTRL_RIGHT :: 0x4335_3b315b1b; + CTRL_LEFT :: 0x4435_3b315b1b; + + SHIFT_UP :: 0x4132_3b315b1b; + SHIFT_DOWN :: 0x4232_3b315b1b; + SHIFT_RIGHT :: 0x4332_3b315b1b; + SHIFT_LEFT :: 0x4432_3b315b1b; + + CTRL_SHIFT_UP :: 0x4136_3b315b1b; + CTRL_SHIFT_DOWN :: 0x4236_3b315b1b; + CTRL_SHIFT_RIGHT:: 0x4336_3b315b1b; + CTRL_SHIFT_LEFT :: 0x4436_3b315b1b; + + ALT_UP :: 0x4133_3b315b1b; + ALT_DOWN :: 0x4233_3b315b1b; + ALT_RIGHT :: 0x4333_3b315b1b; + ALT_LEFT :: 0x4433_3b315b1b; + + CTRL_C :: 0x03; + CTRL_V :: 0x16; + CTRL_X :: 0x18; + CTRL_Y :: 0x19; + CTRL_Z :: 0x1A; + CTRL_BACKSLASH :: 0x1C; + + // ALT_SHIFT_UP :: 0x4133_3b315b1b; + // ALT_SHIFT_DOWN :: 0x4233_3b315b1b; + // ALT_SHIFT_RIGHT :: 0x4333_3b315b1b; + // ALT_SHIFT_LEFT :: 0x4433_3b315b1b; + + ENTER :: 0x0D; + ESCAPE :: 0x1B; + BACKSPACE :: 0x7F; + DELETE :: 0x7E335B1B; +} + Color :: enum u8 { RESET :: 0; DEFAULT :: 39; @@ -81,11 +137,40 @@ Color :: enum u8 { BRIGHT_WHITE :: 97; } -__old_logger : type_of(context.logger); +update_terminal_size :: () { + TIOCGWINSZ :: 0x5413; + winsize : struct { + ws_row, ws_col, ws_xpixel, ws_ypixel : u16; + } + ioctl(0, TIOCGWINSZ, *winsize); + terminal_state.width = xx winsize.ws_col; + terminal_state.height = xx winsize.ws_row; +} initialize :: () { #if OS == .LINUX { - tcgetattr(STDIN_FILENO, *__term); + + tui_write("\e[?25l"); // hide cursor + tui_write("\e7"); // save cursor position + tui_write("\e[?1047h"); // switch screen + tui_write("\e[?30l"); // hide scrollbar + + tui_write("\e[H"); // move to top left corner + tui_write("\e[0m"); // set default mode + tui_write("\e[2J"); // clear screen + { + tcgetattr(STDIN_FILENO, *__term); + term_new := __term; + + 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; + + tcsetattr(STDIN_FILENO, 0, *term_new); + } + update_terminal_size(); } else { assert(false, "procedure call on unsupported OS\n"); } @@ -95,22 +180,47 @@ terminate :: () { #if OS == .LINUX { tcsetattr(STDIN_FILENO, 0, *__term); // return echo - tio_write("\e[?47l"); // restore screen - tio_write("\e8"); // restore cursor - tio_write("\e[?25h"); // show cursor - tio_write("\e[?30h"); // show scrollbar + tui_write("\e[?47l"); // restore screen + tui_write("\e8"); // restore cursor + tui_write("\e[?25h"); // show cursor + tui_write("\e[?30h"); // show scrollbar terminal_state = .{}; - context.logger = __old_logger; } else { assert(false, "procedure call on unsupported OS\n"); } } -tio_write :: (str : string) { +tui_write :: (str : string) { #if OS == .LINUX { write(STDIN_FILENO, str.data, xx str.count); } else { assert(false, "procedure call on unsupported OS\n"); } } + +buffer: [..] Key; + +tui_ungetch :: (key: Key) { + array_add(*buffer, key); +} + +tui_getch :: (block := true) -> Key { + #if OS == .LINUX { + buf : Key = xx 0; + if buffer.count > 0 return pop(*buffer); + l := read(STDIN_FILENO, (cast(*u8)*buf), 8); //!!! + check_signal :: inline (key : Key) { + #if ENABLE_SIGINT if key == .CTRL_C raise(SIGINT); //!!! + #if ENABLE_SIGQUIT if key == .CTRL_BACKSLASH raise(SIGQUIT); + } + check_signal(buf); + return ifx l <= 0 then Key.READ_ERROR else buf; + } else { + return .READ_ERROR; + } +} + +tui_clear_screen :: inline () { + tui_write("\e[2J"); +} diff --git a/ttt.jai b/ttt.jai index b3a1cd3..7431ac6 100644 --- a/ttt.jai +++ b/ttt.jai @@ -25,9 +25,9 @@ #import "File"; #import "File_Utilities"; #import "String"; -//#import "curses"; +// #import "curses"; // #import "kscurses"; -TIO :: #import "tio"; +TUI :: #import "tio"; // TODO Rename module to "tui.jai" #load "Integer_Saturating_Arithmetic.jai"; @@ -36,7 +36,6 @@ TIO :: #import "tio"; stdscr: *void; // TODO DAM A_BOLD: s32 = 0; // TODO DAM COLOR_PAIR :: (a: s32) -> s32 { return 0; } // TODO DAM -ERR :: -1; // TODO DAM KEY_RESIZE :: 410; // TODO DAM KEY_F2 :: 266; // TODO DAM KEY_F5 :: 269; // TODO DAM @@ -902,13 +901,12 @@ initialize_tui :: () { } // TODO DAM - TIO.initialize(); - TIO.terminate(); // TODO DAM - //stdscr = initscr(); // Start curses mode. TODO DAM - //cbreak(); // Line buffering disabled; pass on everty thing to me. TODO DAM - //keypad(stdscr, true); // I need those nifty F1..F12. TODO DAM - //curs_set(0); // Set cursor invisible. TODO DAM - //noecho(); // Disable echoing input characters. TODO DAM + TUI.initialize(); + // stdscr = initscr(); // Start curses mode. TODO DAM + // cbreak(); // Line buffering disabled; pass on everty thing to me. TODO DAM + // keypad(stdscr, true); // I need those nifty F1..F12. TODO DAM + // curs_set(0); // Set cursor invisible. TODO DAM + // noecho(); // Disable echoing input characters. TODO DAM // Initialize pairs of colors. //start_color(); TODO DAM @@ -957,10 +955,11 @@ draw_tui :: (db: *Database, layout: *Layout) { // Reset theme and clear screen. //attrset(A_NORMAL); TODO DAM - //erase(); TODO DAM + TUI.tui_clear_screen(); // Draw outer border. //box(stdscr, 0, 0); TODO DAM + // WIP // Draw table grids. // TODO Maybe this could be simplified? @@ -1362,7 +1361,7 @@ main :: () { layout := *layouts[Layouts.COMPACT]; //flushinp(); TODO DAM - //ungetch(KEY_RESIZE); TODO DAM + TUI.tui_ungetch(KEY_RESIZE); // TODO DAM while (true) { if (is_terminal_too_small) { @@ -1376,7 +1375,7 @@ main :: () { reset_temporary_storage(); //timeout(INPUT_TIMEOUT_MS); TODO DAM - key := 0; // getch(); TODO DAM + key := TUI.tui_getch(); // getch(); TODO DAM if key == #char "q" || key == #char "Q" break; update_times(*database); //timeout(INPUT_AWAIT_INF); TODO DAM @@ -1400,7 +1399,7 @@ main :: () { if key == { // When getch() times out. - case ERR; + case .READ_ERROR; if (is_autosave_enabled && countdown_to_autosave > 0) { countdown_to_autosave -= INPUT_TIMEOUT_MS; if (countdown_to_autosave <= 0) { @@ -1414,8 +1413,13 @@ main :: () { // When terminal is resized. case KEY_RESIZE; - //clear(); TODO DAM - //getmaxyx(stdscr, *size_y, *size_x); TODO DAM + TUI.tui_clear_screen(); + //getmaxyx(stdscr, *size_y, *size_x); + TUI.update_terminal_size(); // TODO DAM + size_x = TUI.terminal_state.width; + size_y = TUI.terminal_state.height; + print("resize:%x%", size_x, size_y); + asd := TUI.tui_getch(); is_terminal_too_small = size_x < 60 || size_y < 3; update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; @@ -1495,7 +1499,7 @@ main :: () { if (selected_task == null) continue; // Prepare position to input time operation. - selected_day := key - #char "1"; + selected_day := cast(int)(key - #char "1"); // TODO DAM this cast... input_width := layout.columns[L_DAYS_IDX + selected_day].width; input_pos_x := 1 + layout.columns[L_TITLE_IDX].width; @@ -1766,7 +1770,7 @@ main :: () { //getch(); TODO DAM } - //endwin(); TODO DAM + TUI.terminate(); // TODO DAM exit(xx ifx error_saving then 1 else 0); } -- cgit v1.2.3 From 7469ce1ab6c761e81334b918a34972fedba0923e Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 12 Sep 2023 01:04:16 +0100 Subject: Adding a terminal user interface module (like ncurses but way simpler). --- ttt.jai | 60 +++++++++++++++++++++++++++++------- tui.jai | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 11 deletions(-) create mode 100644 tui.jai (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 7431ac6..53a3e01 100644 --- a/ttt.jai +++ b/ttt.jai @@ -27,7 +27,8 @@ #import "String"; // #import "curses"; // #import "kscurses"; -TUI :: #import "tio"; // TODO Rename module to "tui.jai" +TIO :: #import "tio"; // TODO Move things into TUI. +TUI :: #import "tui"; #load "Integer_Saturating_Arithmetic.jai"; @@ -901,7 +902,7 @@ initialize_tui :: () { } // TODO DAM - TUI.initialize(); + TIO.initialize(); // stdscr = initscr(); // Start curses mode. TODO DAM // cbreak(); // Line buffering disabled; pass on everty thing to me. TODO DAM // keypad(stdscr, true); // I need those nifty F1..F12. TODO DAM @@ -955,7 +956,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Reset theme and clear screen. //attrset(A_NORMAL); TODO DAM - TUI.tui_clear_screen(); + TIO.tui_clear_screen(); // Draw outer border. //box(stdscr, 0, 0); TODO DAM @@ -1179,6 +1180,43 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo } main :: () { + + // 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 + TUI.start(); + TUI.clear_screen(); + TUI.draw_box(1,1,13,13); + sleep_milliseconds(10000); + write_string(TUI.Commands.ShowCursor); + TUI.stop(); + return; + + str: string; + + write_string("\e(0"); // Enter Line drawing mode + write_string("\e[104;93m"); // bright yellow on bright blue + write_string("x"); // in line drawing mode, \x78 -> \u2502 "Vertical Bar" + write_string("\e[10m"); // restore color + write_string("\e(B"); // exit line drawing mode + sleep_milliseconds(1000); + + + // str = "\e[sWOW\e[u\e[1P"; + str = "\e[sWOW"; + // write(STDIN_FILENO, str.data, xx str.count); + write_string(str); + sleep_milliseconds(1000); + + str = "\e[?25l"; + // write(STDIN_FILENO, str.data, xx str.count); + write_string(str); + sleep_milliseconds(1000); + + str = "\e[?25h"; + // write(STDIN_FILENO, str.data, xx str.count); + write_string(str); + return; // TODO Implement signal handling and see modules/Debug.jai for examples. @@ -1361,7 +1399,7 @@ main :: () { layout := *layouts[Layouts.COMPACT]; //flushinp(); TODO DAM - TUI.tui_ungetch(KEY_RESIZE); // TODO DAM + TIO.tui_ungetch(KEY_RESIZE); // TODO DAM while (true) { if (is_terminal_too_small) { @@ -1375,7 +1413,7 @@ main :: () { reset_temporary_storage(); //timeout(INPUT_TIMEOUT_MS); TODO DAM - key := TUI.tui_getch(); // getch(); TODO DAM + key := TIO.tui_getch(); // getch(); TODO DAM if key == #char "q" || key == #char "Q" break; update_times(*database); //timeout(INPUT_AWAIT_INF); TODO DAM @@ -1413,13 +1451,13 @@ main :: () { // When terminal is resized. case KEY_RESIZE; - TUI.tui_clear_screen(); + TIO.tui_clear_screen(); //getmaxyx(stdscr, *size_y, *size_x); - TUI.update_terminal_size(); // TODO DAM - size_x = TUI.terminal_state.width; - size_y = TUI.terminal_state.height; + TIO.update_terminal_size(); // TODO DAM + size_x = TIO.terminal_state.width; + size_y = TIO.terminal_state.height; print("resize:%x%", size_x, size_y); - asd := TUI.tui_getch(); + asd := TIO.tui_getch(); is_terminal_too_small = size_x < 60 || size_y < 3; update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; @@ -1770,7 +1808,7 @@ main :: () { //getch(); TODO DAM } - TUI.terminate(); // TODO DAM + TIO.terminate(); // TODO DAM exit(xx ifx error_saving then 1 else 0); } diff --git a/tui.jai b/tui.jai new file mode 100644 index 0000000..a0eb025 --- /dev/null +++ b/tui.jai @@ -0,0 +1,106 @@ +#import "Basic"; + +Drawings :: struct { + CornerBR :: "\x6A"; + CornerTR :: "\x6B"; + CornerTL :: "\x6C"; + CornerBL :: "\x6D"; + Cross :: "\x6E"; + LineH :: "\x71"; + TeeL :: "\x74"; + TeeR :: "\x75"; + TeeB :: "\x76"; + TeeT :: "\x77"; + LineV :: "\x78"; +} + +Commands :: struct { + EnterAlternateBuffer :: "\e[?1049h"; + EnterMainBuffer :: "\e[?1049l"; + + EnterDrawingMode :: "\e(0"; + EnterNormalMode :: "\e(B"; + ClearScreen :: "\e[2J"; + + // Cursor Visibility + ShowCursor :: "\e[?25h"; + HideCursor :: "\e[?25l"; + StartBlinking :: "\e[?25h]"; + StopBlinking :: "\e[?25l]"; + + // 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 + QueryCursorPos :: "\e[6n"; // Emits the cursor position as: ESC [ ; R Where = row and = column. + QueryDeviceAttributes :: "\e[0c"; +} + +start :: () { + write_string(Commands.EnterAlternateBuffer); +} + +stop :: () { + write_string(Commands.EnterMainBuffer); +} + +draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false) { + + write_string(Commands.HideCursor); + write_strings( + // Commands.EnterNormalMode, + Commands.EnterDrawingMode, + "\e[1;1H", // Move to position 1,1 + // TODO // Move pointer to top-left corner. + Drawings.CornerTL, + to_standard_error = to_standard_error); + + for 1..width-2 { + write_string(Drawings.LineH, to_standard_error = to_standard_error); + } + write_string(Drawings.CornerTR, to_standard_error = to_standard_error); + + + // TODO Take care of the temporary allocations. + for idx: 2..height-1 { + tmpL := tprint("\e[%;%H", idx, 1); + tmpR := tprint("\e[%;%H", idx, width); + write_strings( + tmpL, + Drawings.LineV, + tmpR, + Drawings.LineV, + to_standard_error = to_standard_error); + } + + tmpBL := tprint("\e[%;%H", height, 1); + write_strings( + tmpBL, + Drawings.CornerBL, + to_standard_error = to_standard_error); + for 1..width-2 { + write_string(Drawings.LineH, to_standard_error = to_standard_error); + } + write_string(Drawings.CornerBR, to_standard_error = to_standard_error); + + write_strings( + // TODO // print + Commands.EnterNormalMode, + to_standard_error = to_standard_error); +} + +clear_screen :: inline () { + write_string(Commands.ClearScreen); +} -- cgit v1.2.3 From ee1a68abb41ec7d0f76b458b5340d08a3f881a2b Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 15 Sep 2023 02:24:05 +0100 Subject: Added QueryWindowSizeInChars to tui.jai. --- ttt.jai | 12 ++++++++---- tui.jai | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 53a3e01..8ac98c8 100644 --- a/ttt.jai +++ b/ttt.jai @@ -21,7 +21,7 @@ #import "System"; #import "Sort"; #import "Math"; -#import "POSIX"; +// #import "POSIX"; #import "File"; #import "File_Utilities"; #import "String"; @@ -1186,10 +1186,14 @@ main :: () { // https://github.com/MicrosoftDocs/Console-Docs/blob/main/docs/console-virtual-terminal-sequences.md TUI.start(); TUI.clear_screen(); - TUI.draw_box(1,1,13,13); - sleep_milliseconds(10000); - write_string(TUI.Commands.ShowCursor); + TUI.draw_box(1,1,24,13); + // TODO get size of console... its being displayed... but we want to collect it. + // write_string(TUI.Commands.QueryCursorPosition); + write_string(TUI.Commands.QueryWindowSizeInChars); + sleep_milliseconds(3000); + // write_string(TUI.Commands.ShowCursor); TUI.stop(); + // write_string(TUI.Commands.QueryDeviceAttributes); return; str: string; diff --git a/tui.jai b/tui.jai index a0eb025..3c868ee 100644 --- a/tui.jai +++ b/tui.jai @@ -12,6 +12,16 @@ Drawings :: struct { TeeB :: "\x76"; TeeT :: "\x77"; LineV :: "\x78"; + + Blank :: "\x5F"; + Diamond :: "\x60"; + Checkerboard :: "\x61"; + PlusMinus :: "\x67"; + LessThanOrEqual :: "\x79"; + GreaterThanOrEqual :: "\x7A"; + Pi :: "\x7B"; + NotEqual :: "\x7C"; + CenteredDot :: "\x7E"; } Commands :: struct { @@ -21,6 +31,11 @@ Commands :: struct { EnterDrawingMode :: "\e(0"; EnterNormalMode :: "\e(B"; ClearScreen :: "\e[2J"; + ClearLine :: "\e[2K"; + + RefreshWindow :: "\e[7t"; // TODO Not yet tested. + + SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE // Cursor Visibility ShowCursor :: "\e[?25h"; @@ -44,21 +59,22 @@ Commands :: struct { CursorNormalMode :: "\e[?1l"; // Query State - QueryCursorPos :: "\e[6n"; // Emits the cursor position as: ESC [ ; R Where = row and = column. + QueryCursorPosition :: "\e[6n"; // Emits the cursor position as: "ESC [ ; R" Where = row and = column. QueryDeviceAttributes :: "\e[0c"; + QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. + } start :: () { - write_string(Commands.EnterAlternateBuffer); + write_strings(Commands.EnterAlternateBuffer, Commands.SetUTF8, Commands.HideCursor); } stop :: () { - write_string(Commands.EnterMainBuffer); + write_strings(Commands.EnterMainBuffer, Commands.ShowCursor); } draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false) { - write_string(Commands.HideCursor); write_strings( // Commands.EnterNormalMode, Commands.EnterDrawingMode, -- cgit v1.2.3 From 470702f3ef0645b47c11ff54b9a6b0a5e9e8caab Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 19 Sep 2023 01:58:23 +0100 Subject: First implementation of vterm window size query. --- ttt.jai | 10 +++--- tui.jai | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 6 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 8ac98c8..bb17489 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1186,14 +1186,16 @@ main :: () { // https://github.com/MicrosoftDocs/Console-Docs/blob/main/docs/console-virtual-terminal-sequences.md TUI.start(); TUI.clear_screen(); - TUI.draw_box(1,1,24,13); + rows, columns := TUI.get_cena(); + TUI.draw_box(1,1,columns, rows); // TODO get size of console... its being displayed... but we want to collect it. // write_string(TUI.Commands.QueryCursorPosition); - write_string(TUI.Commands.QueryWindowSizeInChars); + // write_string(TUI.Commands.QueryWindowSizeInChars); + // wow := TUI.read_input(); sleep_milliseconds(3000); - // write_string(TUI.Commands.ShowCursor); TUI.stop(); - // write_string(TUI.Commands.QueryDeviceAttributes); + + print("--- --- ---\nRxC = %x%\n", rows, columns); return; str: string; diff --git a/tui.jai b/tui.jai index 3c868ee..564e9ba 100644 --- a/tui.jai +++ b/tui.jai @@ -1,4 +1,5 @@ #import "Basic"; +#import "String"; Drawings :: struct { CornerBR :: "\x6A"; @@ -42,6 +43,8 @@ Commands :: struct { HideCursor :: "\e[?25l"; StartBlinking :: "\e[?25h]"; StopBlinking :: "\e[?25l]"; + SaveCursorPosition :: "\e7"; + RestoreCursorPosition :: "\e8"; // Cursor Shape DefaultShape :: "\e[0 q"; @@ -66,11 +69,25 @@ Commands :: struct { } start :: () { - write_strings(Commands.EnterAlternateBuffer, Commands.SetUTF8, Commands.HideCursor); + // TODO Required to do unlocking input. + tcgetattr(STDIN_FILENO, *__term); + term_new := __term; + + 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; + + tcsetattr(STDIN_FILENO, 0, *term_new); + + write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); } stop :: () { - write_strings(Commands.EnterMainBuffer, Commands.ShowCursor); + write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); + + tcsetattr(STDIN_FILENO, 0, *__term); // return echo } draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false) { @@ -120,3 +137,91 @@ draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false clear_screen :: inline () { write_string(Commands.ClearScreen); } + +// read_input: () -> string; + +#if OS == .LINUX { + #import "Basic"; + #import "POSIX"; + + __term : My_Termios; + + My_Termios :: struct { + c_iflag : u32; // Input modes. + c_oflag : u32; // Output modes. + c_cflag : u32; // Control modes. + c_lflag : u32; // Local modes. + unknown_pad : u8; + c_cc : [32]u8; // Special characters. + c_ispeed : u32; // Input baud rates. + c_ospeed : u32; // Output baud rates. + } + + // Required to do unlocking input. + libc :: #system_library "libc"; + tcsetattr :: (fd : s32, optional_actions : s32, termios_p : *My_Termios) -> s32 #foreign libc; + tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; + + + + read_input :: () -> string { + buffer: [8192] u8; + bytes_read := read(STDIN_FILENO, buffer.data, buffer.count-1); + str := to_string(buffer.data, bytes_read); + return str; + }; + + get_cena :: () -> 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); + + // Result: [8;79;156t + assert( + buffer.data[0] == #char "\e" && + buffer.data[1] == #char "[" && + buffer.data[2] == #char "8", + "Query windows size in chars returned invalid response."); + + parts := split(str, ";"); + rows := parse_int(*parts[1]); + columns := parse_int(*parts[2]); + return rows, columns; + } + +} +else #if OS == .WINDOWS { + #import "Windows"; + + kernel32 :: #system_library "kernel32"; + + stdin, stdout : HANDLE; + + stdin = GetStdHandle( STD_INPUT_HANDLE ); + + ReadConsoleA :: ( + hConsoleHandle: HANDLE, + buff : *u8, + chars_to_read : s32, + chars_read : *s32, + lpInputControl := *void + ) -> bool #foreign kernel32; + + // #run read_input = () -> string { + read_input :: () -> string { + MAX_BYTES_TO_READ :: 1024; + temp : [MAX_BYTES_TO_READ] u8; + result: string = ---; + bytes_read : s32; + + if !ReadConsoleA( stdin, temp.data, xx temp.count, *bytes_read ) + return ""; + + result.data = alloc(bytes_read); + result.count = bytes_read; + memcpy(result.data, temp.data, bytes_read); + return result; + }; +} -- cgit v1.2.3 From 9e2fc467ad0e779734d836656875cf92bcb5732a Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 20 Sep 2023 00:45:44 +0100 Subject: Prepare TUI for windows. --- tio.jai | 62 ++++++++++++++----- ttt.jai | 18 +++--- tui.jai | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 228 insertions(+), 68 deletions(-) (limited to 'ttt.jai') diff --git a/tio.jai b/tio.jai index f6ee0cb..9ccd04b 100644 --- a/tio.jai +++ b/tio.jai @@ -39,8 +39,11 @@ terminal_state : struct { // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; } +else #if OS == .WINDOWS { + #run print("TODO tio\n", to_standard_error = true); +} else { - assert(false, "unsupported OS\n"); + #run assert(false, "unsupported OS\n"); } Graphics_Mode :: struct { @@ -138,13 +141,21 @@ Color :: enum u8 { } update_terminal_size :: () { - TIOCGWINSZ :: 0x5413; - winsize : struct { - ws_row, ws_col, ws_xpixel, ws_ypixel : u16; + #if OS == .LINUX { + TIOCGWINSZ :: 0x5413; + winsize : struct { + ws_row, ws_col, ws_xpixel, ws_ypixel : u16; + } + ioctl(0, TIOCGWINSZ, *winsize); + terminal_state.width = xx winsize.ws_col; + terminal_state.height = xx winsize.ws_row; + } + else #if OS == .WINDOWS { + print("TODO tio\n", to_standard_error = true); + } + else { + assert(false, "unsupported OS\n"); } - ioctl(0, TIOCGWINSZ, *winsize); - terminal_state.width = xx winsize.ws_col; - terminal_state.height = xx winsize.ws_row; } initialize :: () { @@ -170,10 +181,14 @@ initialize :: () { tcsetattr(STDIN_FILENO, 0, *term_new); } - update_terminal_size(); - } else { - assert(false, "procedure call on unsupported OS\n"); - } + update_terminal_size(); + } + else #if OS == .WINDOWS { + print("TODO tio\n", to_standard_error = true); + } + else { + assert(false, "unsupported OS\n"); + } } terminate :: () { @@ -185,17 +200,24 @@ terminate :: () { tui_write("\e[?25h"); // show cursor tui_write("\e[?30h"); // show scrollbar terminal_state = .{}; - - } else { - assert(false, "procedure call on unsupported OS\n"); + } + else #if OS == .WINDOWS { + print("TODO tio\n", to_standard_error = true); + } + else { + assert(false, "unsupported OS\n"); } } tui_write :: (str : string) { #if OS == .LINUX { write(STDIN_FILENO, str.data, xx str.count); - } else { - assert(false, "procedure call on unsupported OS\n"); + } + else #if OS == .WINDOWS { + #run print("TODO tio\n", to_standard_error = true); + } + else { + #run assert(false, "unsupported OS\n"); } } @@ -216,7 +238,13 @@ tui_getch :: (block := true) -> Key { } check_signal(buf); return ifx l <= 0 then Key.READ_ERROR else buf; - } else { + } + else #if OS == .WINDOWS { + print("TODO tio\n", to_standard_error = true); + return .READ_ERROR; + } + else { + assert(false, "unsupported OS\n"); return .READ_ERROR; } } diff --git a/ttt.jai b/ttt.jai index bb17489..588be28 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1181,23 +1181,17 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo main :: () { - // 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 + // -- -- -- TODO WIP Testing TUI -- START TUI.start(); TUI.clear_screen(); - rows, columns := TUI.get_cena(); + rows, columns := TUI.get_buffer_size(); TUI.draw_box(1,1,columns, rows); - // TODO get size of console... its being displayed... but we want to collect it. - // write_string(TUI.Commands.QueryCursorPosition); - // write_string(TUI.Commands.QueryWindowSizeInChars); - // wow := TUI.read_input(); - sleep_milliseconds(3000); + sleep_milliseconds(1500); TUI.stop(); - - print("--- --- ---\nRxC = %x%\n", rows, columns); + print("\nr:c = %:%\n", rows, columns); return; + str: string; write_string("\e(0"); // Enter Line drawing mode @@ -1223,6 +1217,8 @@ main :: () { // write(STDIN_FILENO, str.data, xx str.count); write_string(str); return; + // -- -- -- TODO WIP Testing TUI -- STOP + // TODO Implement signal handling and see modules/Debug.jai for examples. diff --git a/tui.jai b/tui.jai index 564e9ba..e2ecaaf 100644 --- a/tui.jai +++ b/tui.jai @@ -1,6 +1,27 @@ + +// TODO Change this into a module with subfiles windows.jai and unix.jai +// #if OS == .WINDOWS { +// #load "windows.jai"; +// } else #if (OS == .LINUX) || (OS == .MACOS) { +// #load "unix.jai"; +// } else { +// #assert(false, "Unsupported OS."); +// } + + +// TODO On OS validations, use .LINUX or .MACOS to allow MACOS users to enj... errmmm test this. + #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"; @@ -69,29 +90,85 @@ Commands :: struct { } start :: () { - // TODO Required to do unlocking input. - tcgetattr(STDIN_FILENO, *__term); - term_new := __term; - - 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; - - tcsetattr(STDIN_FILENO, 0, *term_new); - + #if OS == .LINUX { + // TODO Required to do unlocking input. + tcgetattr(STDIN_FILENO, *__term); + term_new := __term; + + 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; + + tcsetattr(STDIN_FILENO, 0, *term_new); + } + else { + print("TODO TUI\n", to_standard_error = true); + + + // stdin + stdin = GetStdHandle(STD_INPUT_HANDLE ); + if stdin == INVALID_HANDLE_VALUE { + print("Invalid input handler.", to_standard_error = true); + return; + } + if GetConsoleMode(stdin, *initial_stdin_mode) == false { + print("Failed to get input mode.", to_standard_error = true); + return; + } + if SetConsoleMode(stdin, initial_stdin_mode | ENABLE_VIRTUAL_TERMINAL_INPUT) == false { + print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); + return; + } + + // stdout + stdout = GetStdHandle(STD_OUTPUT_HANDLE); + outMode: u32 = 0; + if stdout == INVALID_HANDLE_VALUE { + print("Invalid output handler.", to_standard_error = true); + return; + } + if GetConsoleMode(stdout, *initial_stdout_mode) == false { + print("Failed to get output mode.", to_standard_error = true); + return; + } + if SetConsoleMode(stdout, initial_stdout_mode | ENABLE_PROCESSED_OUTPUT| ENABLE_VIRTUAL_TERMINAL_PROCESSING) == false { + print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); + return; + } + } write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); + isTUIActive = true; } stop :: () { + isTUIActive = false; write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); - tcsetattr(STDIN_FILENO, 0, *__term); // return echo + #if OS == .LINUX { + tcsetattr(STDIN_FILENO, 0, *__term); // return echo + } + else { + print("TODO TUI\n", to_standard_error = true); + + if SetConsoleMode(stdin, initial_stdin_mode) == false { + print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); + return; + } + + if SetConsoleMode(stdout, initial_stdout_mode) == false { + print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); + return; + } + } } draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false) { + + // TODO Hardcoded box starting at 1,1... fix this! + write_strings( // Commands.EnterNormalMode, Commands.EnterDrawingMode, @@ -138,29 +215,41 @@ clear_screen :: inline () { write_string(Commands.ClearScreen); } + +// get_cursor_position :: () -> row: int, column: int { +// assert(isTUIActive, "TUI is not active."); // TODO +// write_string(TUI.Commands.QueryCursorPosition); // Returned "\e[21;1R" +// read_input() +// } + + // read_input: () -> string; #if OS == .LINUX { #import "Basic"; #import "POSIX"; - __term : My_Termios; - - My_Termios :: struct { - c_iflag : u32; // Input modes. - c_oflag : u32; // Output modes. - c_cflag : u32; // Control modes. - c_lflag : u32; // Local modes. - unknown_pad : u8; - c_cc : [32]u8; // Special characters. - c_ispeed : u32; // Input baud rates. - c_ospeed : u32; // Output baud rates. - } + __term : My_Termios; + + My_Termios :: struct { + c_iflag : u32; // Input mode flags. + c_oflag : u32; // Output mode flags. + c_cflag : u32; // Control modes flags. + c_lflag : u32; // Local modes flags. + c_line : u8; // Line discipline. + c_cc : [32]u8; // Control characters. + c_ispeed : u32; // Input speed (baud rates). + c_ospeed : u32; // Output speed (baud rates). + } // Required to do unlocking input. - libc :: #system_library "libc"; - tcsetattr :: (fd : s32, optional_actions : s32, termios_p : *My_Termios) -> s32 #foreign libc; - tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign 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 : *My_Termios) -> s32 #foreign libc; + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html + tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; @@ -171,13 +260,13 @@ clear_screen :: inline () { return str; }; - get_cena :: () -> rows: int, columns: int { + get_buffer_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); - + // Result: [8;79;156t assert( buffer.data[0] == #char "\e" && @@ -195,19 +284,52 @@ clear_screen :: inline () { else #if OS == .WINDOWS { #import "Windows"; + ENABLE_VIRTUAL_TERMINAL_INPUT :: 0x0200; + + ENABLE_PROCESSED_OUTPUT :: 0x0001; + ENABLE_WRAP_AT_EOL_OUTPUT :: 0x0002; + ENABLE_VIRTUAL_TERMINAL_PROCESSING :: 0x0004; + DISABLE_NEWLINE_AUTO_RETURN :: 0x0008; + ENABLE_LVB_GRID_WORLDWIDE :: 0x0010; + + SHORT :: s16; + WORD :: u16; + DWORD :: s32; + + 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; + } + kernel32 :: #system_library "kernel32"; - - stdin, stdout : HANDLE; - stdin = GetStdHandle( STD_INPUT_HANDLE ); + GetConsoleScreenBufferInfo :: (hConsoleOutput: HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) -> bool #foreign kernel32; + ReadConsoleA :: (hConsoleHandle: HANDLE, buff : *u8, chars_to_read : s32, chars_read : *s32, lpInputControl := *void ) -> bool #foreign kernel32; + // ReadConsole :: (hConsoleInput: HANDLE, lpBuffer: *u8, nNumberOfCharsToRead: s32, lpNumberOfCharsRead: *s32, pInputControl := *void) -> bool #foreign kernel32; + GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *u32) -> bool #foreign kernel32; + SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: u32) -> bool #foreign kernel32; + GetLastError :: () -> s32 #foreign kernel32; - ReadConsoleA :: ( - hConsoleHandle: HANDLE, - buff : *u8, - chars_to_read : s32, - chars_read : *s32, - lpInputControl := *void - ) -> bool #foreign kernel32; + stdin: HANDLE; + initial_stdin_mode: u32; + stdout: HANDLE; + initial_stdout_mode: u32; // #run read_input = () -> string { read_input :: () -> string { @@ -224,4 +346,18 @@ else #if OS == .WINDOWS { memcpy(result.data, temp.data, bytes_read); return result; }; + + get_buffer_size :: () -> rows: int, columns: int { + + // GET WINDOW CHAR SIZE + + + ScreenBufferInfo: CONSOLE_SCREEN_BUFFER_INFO; + GetConsoleScreenBufferInfo(stdout, *ScreenBufferInfo); + Size: COORD; + Size.X = ScreenBufferInfo.srWindow.Right - ScreenBufferInfo.srWindow.Left + 1; + Size.Y = ScreenBufferInfo.srWindow.Bottom - ScreenBufferInfo.srWindow.Top + 1; + + return Size.Y, Size.X; + } } -- cgit v1.2.3 From 65adffcf572c98c0198affcdf729d989fec253ae Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 20 Sep 2023 18:00:20 +0100 Subject: Moved TUI into a module with split OS-based implementations. --- TUI/module.jai | 194 ++++++++++++++++++++++++++++++ TUI/unix.jai | 63 ++++++++++ TUI/windows.jai | 111 +++++++++++++++++ ttt.jai | 4 +- tui.jai | 363 -------------------------------------------------------- 5 files changed, 370 insertions(+), 365 deletions(-) create mode 100644 TUI/module.jai create mode 100644 TUI/unix.jai create mode 100644 TUI/windows.jai delete mode 100644 tui.jai (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai new file mode 100644 index 0000000..67a9edd --- /dev/null +++ b/TUI/module.jai @@ -0,0 +1,194 @@ +#if OS == .WINDOWS { + #load "windows.jai"; +} else #if (OS == .LINUX) || (OS == .MACOS) { + #load "unix.jai"; +} else { + #assert(false, "Unsupported OS."); +} + +#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"; + CornerTL :: "\x6C"; + CornerBL :: "\x6D"; + Cross :: "\x6E"; + LineH :: "\x71"; + TeeL :: "\x74"; + TeeR :: "\x75"; + TeeB :: "\x76"; + TeeT :: "\x77"; + LineV :: "\x78"; + + Blank :: "\x5F"; + Diamond :: "\x60"; + Checkerboard :: "\x61"; + PlusMinus :: "\x67"; + LessThanOrEqual :: "\x79"; + GreaterThanOrEqual :: "\x7A"; + Pi :: "\x7B"; + NotEqual :: "\x7C"; + CenteredDot :: "\x7E"; +} + +Commands :: struct { + EnterAlternateBuffer :: "\e[?1049h"; + EnterMainBuffer :: "\e[?1049l"; + + EnterDrawingMode :: "\e(0"; + EnterNormalMode :: "\e(B"; + ClearScreen :: "\e[2J"; + ClearLine :: "\e[2K"; + + RefreshWindow :: "\e[7t"; // TODO Not yet tested. + + SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE + + // 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" Where = row and = column. + QueryDeviceAttributes :: "\e[0c"; + QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. + +} + +start :: () { + OS_prepare_terminal(); + write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); + isTUIActive = true; +} + +stop :: () { + isTUIActive = false; + write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); + OS_reset_terminal(); +} + +draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false) { + + + // TODO Hardcoded box starting at 1,1... fix this! + + write_strings( + // Commands.EnterNormalMode, + Commands.EnterDrawingMode, + "\e[1;1H", // Move to position 1,1 + // TODO // Move pointer to top-left corner. + Drawings.CornerTL, + to_standard_error = to_standard_error); + + for 1..width-2 { + write_string(Drawings.LineH, to_standard_error = to_standard_error); + } + write_string(Drawings.CornerTR, to_standard_error = to_standard_error); + + + // TODO Take care of the temporary allocations. + for idx: 2..height-1 { + tmpL := tprint("\e[%;%H", idx, 1); + tmpR := tprint("\e[%;%H", idx, width); + write_strings( + tmpL, + Drawings.LineV, + tmpR, + Drawings.LineV, + to_standard_error = to_standard_error); + } + + tmpBL := tprint("\e[%;%H", height, 1); + write_strings( + tmpBL, + Drawings.CornerBL, + to_standard_error = to_standard_error); + for 1..width-2 { + write_string(Drawings.LineH, to_standard_error = to_standard_error); + } + write_string(Drawings.CornerBR, to_standard_error = to_standard_error); + + write_strings( + // TODO // print + Commands.EnterNormalMode, + to_standard_error = to_standard_error); +} + +clear_screen :: inline () { + write_string(Commands.ClearScreen); +} + +get_terminal_size :: () -> rows: int, columns: int { + rows, columns := OS_get_terminal_size(); + return rows, columns; +} + +// read_input: () -> string { + // return OS_read_input(); +// } + +// get_cursor_position :: () -> row: int, column: int { +// assert(isTUIActive, "TUI is not active."); // TODO +// write_string(TUI.Commands.QueryCursorPosition); // Returned "\e[21;1R" +// read_input() +// } + + +// read_input: () -> string; + +#if OS == .WINDOWS { + + // #run read_input = () -> string { + read_input :: () -> string { + MAX_BYTES_TO_READ :: 1024; + temp : [MAX_BYTES_TO_READ] u8; + result: string = ---; + bytes_read : s32; + + if !ReadConsoleA( stdin, temp.data, xx temp.count, *bytes_read ) + return ""; + + result.data = alloc(bytes_read); + result.count = bytes_read; + memcpy(result.data, temp.data, bytes_read); + return result; + }; +} +else #if OS == .LINUX || OS == .MACOS { + #import "Basic"; + #import "POSIX"; + + read_input :: () -> string { + buffer: [8192] u8; + bytes_read := read(STDIN_FILENO, buffer.data, buffer.count-1); + str := to_string(buffer.data, bytes_read); + return str; + }; +} diff --git a/TUI/unix.jai b/TUI/unix.jai new file mode 100644 index 0000000..439447d --- /dev/null +++ b/TUI/unix.jai @@ -0,0 +1,63 @@ +#import "POSIX"; + + + __term : My_Termios; + + My_Termios :: struct { + c_iflag : u32; // Input mode flags. + c_oflag : u32; // Output mode flags. + c_cflag : u32; // Control modes flags. + c_lflag : u32; // Local modes flags. + c_line : u8; // Line discipline. + c_cc : [32]u8; // Control characters. + c_ispeed : u32; // Input speed (baud rates). + 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; + + // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html + tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; + + +OS_prepare_terminal :: () { + // TODO Required to do unlocking input. + tcgetattr(STDIN_FILENO, *__term); + term_new := __term; + + 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; + + tcsetattr(STDIN_FILENO, 0, *term_new); +} + +OS_reset_terminal :: () { + tcsetattr(STDIN_FILENO, 0, *__term); // 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); + + // Result: [8;79;156t + assert( + buffer.data[0] == #char "\e" && + buffer.data[1] == #char "[" && + buffer.data[2] == #char "8", + "Query windows size in chars returned invalid response."); + + parts := split(str, ";"); + rows := parse_int(*parts[1]); + columns := parse_int(*parts[2]); + return rows, columns; +} diff --git a/TUI/windows.jai b/TUI/windows.jai new file mode 100644 index 0000000..ef0cfa8 --- /dev/null +++ b/TUI/windows.jai @@ -0,0 +1,111 @@ +#import "Windows"; + + + kernel32 :: #system_library "kernel32"; + + GetConsoleScreenBufferInfo :: (hConsoleOutput: HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) -> bool #foreign kernel32; + ReadConsoleA :: (hConsoleHandle: HANDLE, buff : *u8, chars_to_read : s32, chars_read : *s32, lpInputControl := *void ) -> bool #foreign kernel32; + // ReadConsole :: (hConsoleInput: HANDLE, lpBuffer: *u8, nNumberOfCharsToRead: s32, lpNumberOfCharsRead: *s32, pInputControl := *void) -> bool #foreign kernel32; + GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *u32) -> bool #foreign kernel32; + SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: u32) -> bool #foreign kernel32; + GetLastError :: () -> s32 #foreign kernel32; + + ENABLE_VIRTUAL_TERMINAL_INPUT :: 0x0200; + + ENABLE_PROCESSED_OUTPUT :: 0x0001; + ENABLE_WRAP_AT_EOL_OUTPUT :: 0x0002; + ENABLE_VIRTUAL_TERMINAL_PROCESSING :: 0x0004; + DISABLE_NEWLINE_AUTO_RETURN :: 0x0008; + ENABLE_LVB_GRID_WORLDWIDE :: 0x0010; + + SHORT :: s16; + WORD :: u16; + DWORD :: s32; + + 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; + } + + + stdin: HANDLE; + initial_stdin_mode: u32; + stdout: HANDLE; + initial_stdout_mode: u32; + + +OS_prepare_terminal :: () { + print("TODO TUI\n", to_standard_error = true); + + + // stdin + stdin = GetStdHandle(STD_INPUT_HANDLE ); + if stdin == INVALID_HANDLE_VALUE { + print("Invalid input handler.", to_standard_error = true); + return; + } + if GetConsoleMode(stdin, *initial_stdin_mode) == false { + print("Failed to get input mode.", to_standard_error = true); + return; + } + if SetConsoleMode(stdin, initial_stdin_mode | ENABLE_VIRTUAL_TERMINAL_INPUT) == false { + print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); + return; + } + + // stdout + stdout = GetStdHandle(STD_OUTPUT_HANDLE); + outMode: u32 = 0; + if stdout == INVALID_HANDLE_VALUE { + print("Invalid output handler.", to_standard_error = true); + return; + } + if GetConsoleMode(stdout, *initial_stdout_mode) == false { + print("Failed to get output mode.", to_standard_error = true); + return; + } + if SetConsoleMode(stdout, initial_stdout_mode | ENABLE_PROCESSED_OUTPUT| ENABLE_VIRTUAL_TERMINAL_PROCESSING) == false { + print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); + return; + } +} + +OS_reset_terminal :: () { + print("TODO TUI\n", to_standard_error = true); + + if SetConsoleMode(stdin, initial_stdin_mode) == false { + print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); + return; + } + + if SetConsoleMode(stdout, initial_stdout_mode) == false { + print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); + return; + } +} + +OS_get_terminal_size :: () -> rows: int, columns: int { + + ScreenBufferInfo: CONSOLE_SCREEN_BUFFER_INFO; + GetConsoleScreenBufferInfo(stdout, *ScreenBufferInfo); + columns := ScreenBufferInfo.srWindow.Right - ScreenBufferInfo.srWindow.Left + 1; + rows := ScreenBufferInfo.srWindow.Bottom - ScreenBufferInfo.srWindow.Top + 1; + + return rows, columns; +} diff --git a/ttt.jai b/ttt.jai index 588be28..0cd8ead 100644 --- a/ttt.jai +++ b/ttt.jai @@ -28,7 +28,7 @@ // #import "curses"; // #import "kscurses"; TIO :: #import "tio"; // TODO Move things into TUI. -TUI :: #import "tui"; +TUI :: #import "TUI"; #load "Integer_Saturating_Arithmetic.jai"; @@ -1184,7 +1184,7 @@ main :: () { // -- -- -- TODO WIP Testing TUI -- START TUI.start(); TUI.clear_screen(); - rows, columns := TUI.get_buffer_size(); + rows, columns := TUI.get_terminal_size(); TUI.draw_box(1,1,columns, rows); sleep_milliseconds(1500); TUI.stop(); diff --git a/tui.jai b/tui.jai deleted file mode 100644 index e2ecaaf..0000000 --- a/tui.jai +++ /dev/null @@ -1,363 +0,0 @@ - -// TODO Change this into a module with subfiles windows.jai and unix.jai -// #if OS == .WINDOWS { -// #load "windows.jai"; -// } else #if (OS == .LINUX) || (OS == .MACOS) { -// #load "unix.jai"; -// } else { -// #assert(false, "Unsupported OS."); -// } - - -// TODO On OS validations, use .LINUX or .MACOS to allow MACOS users to enj... errmmm test this. - -#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"; - CornerTL :: "\x6C"; - CornerBL :: "\x6D"; - Cross :: "\x6E"; - LineH :: "\x71"; - TeeL :: "\x74"; - TeeR :: "\x75"; - TeeB :: "\x76"; - TeeT :: "\x77"; - LineV :: "\x78"; - - Blank :: "\x5F"; - Diamond :: "\x60"; - Checkerboard :: "\x61"; - PlusMinus :: "\x67"; - LessThanOrEqual :: "\x79"; - GreaterThanOrEqual :: "\x7A"; - Pi :: "\x7B"; - NotEqual :: "\x7C"; - CenteredDot :: "\x7E"; -} - -Commands :: struct { - EnterAlternateBuffer :: "\e[?1049h"; - EnterMainBuffer :: "\e[?1049l"; - - EnterDrawingMode :: "\e(0"; - EnterNormalMode :: "\e(B"; - ClearScreen :: "\e[2J"; - ClearLine :: "\e[2K"; - - RefreshWindow :: "\e[7t"; // TODO Not yet tested. - - SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE - - // 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" Where = row and = column. - QueryDeviceAttributes :: "\e[0c"; - QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. - -} - -start :: () { - #if OS == .LINUX { - // TODO Required to do unlocking input. - tcgetattr(STDIN_FILENO, *__term); - term_new := __term; - - 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; - - tcsetattr(STDIN_FILENO, 0, *term_new); - } - else { - print("TODO TUI\n", to_standard_error = true); - - - // stdin - stdin = GetStdHandle(STD_INPUT_HANDLE ); - if stdin == INVALID_HANDLE_VALUE { - print("Invalid input handler.", to_standard_error = true); - return; - } - if GetConsoleMode(stdin, *initial_stdin_mode) == false { - print("Failed to get input mode.", to_standard_error = true); - return; - } - if SetConsoleMode(stdin, initial_stdin_mode | ENABLE_VIRTUAL_TERMINAL_INPUT) == false { - print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); - return; - } - - // stdout - stdout = GetStdHandle(STD_OUTPUT_HANDLE); - outMode: u32 = 0; - if stdout == INVALID_HANDLE_VALUE { - print("Invalid output handler.", to_standard_error = true); - return; - } - if GetConsoleMode(stdout, *initial_stdout_mode) == false { - print("Failed to get output mode.", to_standard_error = true); - return; - } - if SetConsoleMode(stdout, initial_stdout_mode | ENABLE_PROCESSED_OUTPUT| ENABLE_VIRTUAL_TERMINAL_PROCESSING) == false { - print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); - return; - } - } - write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); - isTUIActive = true; -} - -stop :: () { - isTUIActive = false; - write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); - - #if OS == .LINUX { - tcsetattr(STDIN_FILENO, 0, *__term); // return echo - } - else { - print("TODO TUI\n", to_standard_error = true); - - if SetConsoleMode(stdin, initial_stdin_mode) == false { - print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); - return; - } - - if SetConsoleMode(stdout, initial_stdout_mode) == false { - print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); - return; - } - } -} - -draw_box :: (x: int, y: int, width: int, height: int, to_standard_error := false) { - - - // TODO Hardcoded box starting at 1,1... fix this! - - write_strings( - // Commands.EnterNormalMode, - Commands.EnterDrawingMode, - "\e[1;1H", // Move to position 1,1 - // TODO // Move pointer to top-left corner. - Drawings.CornerTL, - to_standard_error = to_standard_error); - - for 1..width-2 { - write_string(Drawings.LineH, to_standard_error = to_standard_error); - } - write_string(Drawings.CornerTR, to_standard_error = to_standard_error); - - - // TODO Take care of the temporary allocations. - for idx: 2..height-1 { - tmpL := tprint("\e[%;%H", idx, 1); - tmpR := tprint("\e[%;%H", idx, width); - write_strings( - tmpL, - Drawings.LineV, - tmpR, - Drawings.LineV, - to_standard_error = to_standard_error); - } - - tmpBL := tprint("\e[%;%H", height, 1); - write_strings( - tmpBL, - Drawings.CornerBL, - to_standard_error = to_standard_error); - for 1..width-2 { - write_string(Drawings.LineH, to_standard_error = to_standard_error); - } - write_string(Drawings.CornerBR, to_standard_error = to_standard_error); - - write_strings( - // TODO // print - Commands.EnterNormalMode, - to_standard_error = to_standard_error); -} - -clear_screen :: inline () { - write_string(Commands.ClearScreen); -} - - -// get_cursor_position :: () -> row: int, column: int { -// assert(isTUIActive, "TUI is not active."); // TODO -// write_string(TUI.Commands.QueryCursorPosition); // Returned "\e[21;1R" -// read_input() -// } - - -// read_input: () -> string; - -#if OS == .LINUX { - #import "Basic"; - #import "POSIX"; - - __term : My_Termios; - - My_Termios :: struct { - c_iflag : u32; // Input mode flags. - c_oflag : u32; // Output mode flags. - c_cflag : u32; // Control modes flags. - c_lflag : u32; // Local modes flags. - c_line : u8; // Line discipline. - c_cc : [32]u8; // Control characters. - c_ispeed : u32; // Input speed (baud rates). - 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; - - // https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/tcgetattr.c.html - tcgetattr :: (fd : s32, termios_p : *My_Termios) -> s32 #foreign libc; - - - - read_input :: () -> string { - buffer: [8192] u8; - bytes_read := read(STDIN_FILENO, buffer.data, buffer.count-1); - str := to_string(buffer.data, bytes_read); - return str; - }; - - get_buffer_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); - - // Result: [8;79;156t - assert( - buffer.data[0] == #char "\e" && - buffer.data[1] == #char "[" && - buffer.data[2] == #char "8", - "Query windows size in chars returned invalid response."); - - parts := split(str, ";"); - rows := parse_int(*parts[1]); - columns := parse_int(*parts[2]); - return rows, columns; - } - -} -else #if OS == .WINDOWS { - #import "Windows"; - - ENABLE_VIRTUAL_TERMINAL_INPUT :: 0x0200; - - ENABLE_PROCESSED_OUTPUT :: 0x0001; - ENABLE_WRAP_AT_EOL_OUTPUT :: 0x0002; - ENABLE_VIRTUAL_TERMINAL_PROCESSING :: 0x0004; - DISABLE_NEWLINE_AUTO_RETURN :: 0x0008; - ENABLE_LVB_GRID_WORLDWIDE :: 0x0010; - - SHORT :: s16; - WORD :: u16; - DWORD :: s32; - - 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; - } - - kernel32 :: #system_library "kernel32"; - - GetConsoleScreenBufferInfo :: (hConsoleOutput: HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) -> bool #foreign kernel32; - ReadConsoleA :: (hConsoleHandle: HANDLE, buff : *u8, chars_to_read : s32, chars_read : *s32, lpInputControl := *void ) -> bool #foreign kernel32; - // ReadConsole :: (hConsoleInput: HANDLE, lpBuffer: *u8, nNumberOfCharsToRead: s32, lpNumberOfCharsRead: *s32, pInputControl := *void) -> bool #foreign kernel32; - GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *u32) -> bool #foreign kernel32; - SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: u32) -> bool #foreign kernel32; - GetLastError :: () -> s32 #foreign kernel32; - - stdin: HANDLE; - initial_stdin_mode: u32; - stdout: HANDLE; - initial_stdout_mode: u32; - - // #run read_input = () -> string { - read_input :: () -> string { - MAX_BYTES_TO_READ :: 1024; - temp : [MAX_BYTES_TO_READ] u8; - result: string = ---; - bytes_read : s32; - - if !ReadConsoleA( stdin, temp.data, xx temp.count, *bytes_read ) - return ""; - - result.data = alloc(bytes_read); - result.count = bytes_read; - memcpy(result.data, temp.data, bytes_read); - return result; - }; - - get_buffer_size :: () -> rows: int, columns: int { - - // GET WINDOW CHAR SIZE - - - ScreenBufferInfo: CONSOLE_SCREEN_BUFFER_INFO; - GetConsoleScreenBufferInfo(stdout, *ScreenBufferInfo); - Size: COORD; - Size.X = ScreenBufferInfo.srWindow.Right - ScreenBufferInfo.srWindow.Left + 1; - Size.Y = ScreenBufferInfo.srWindow.Bottom - ScreenBufferInfo.srWindow.Top + 1; - - return Size.Y, Size.X; - } -} -- cgit v1.2.3 From 1b6fbda3a7f9fa95e2dbbafea56388900167639c Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 21 Sep 2023 09:58:41 +0100 Subject: Fixed draw_box procedure. Added buggy prototype for read_input. --- TUI/module.jai | 175 ++++++++++++++++++++++++++++++++++++++++++++++----------- TUI/unix.jai | 2 +- ttt.jai | 14 +++-- 3 files changed, 152 insertions(+), 39 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 6d65342..27a871b 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -6,6 +6,7 @@ #assert(false, "Unsupported OS."); } +#import "Basic"; #import "String"; // https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences @@ -52,6 +53,9 @@ Commands :: struct { SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE + // Cursor Position + SetCursorPosition :: "\e[%;%H"; + // Cursor Visibility ShowCursor :: "\e[?25h"; HideCursor :: "\e[?25l"; @@ -78,16 +82,18 @@ Commands :: struct { // Query State QueryCursorPosition :: "\e[6n"; // Emits the cursor position as: "ESC [ ; R" Where = row and = column. QueryDeviceAttributes :: "\e[0c"; - QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. + QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. } +// TODO Maybe rename to "setup()" start :: () { OS_prepare_terminal(); write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); isTUIActive = true; } +// TODO Maybe rename to "reset()" stop :: () { isTUIActive = false; write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); @@ -96,14 +102,18 @@ stop :: () { draw_box :: (x: int, y: int, width: int, height: int) { + // 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."); - // TODO Hardcoded box starting at 1,1... fix this! - + auto_release_temp(); + + tmp_string: string; + + tmp_string = tprint(Commands.SetCursorPosition, y, x); write_strings( - // Commands.EnterNormalMode, Commands.EnterDrawingMode, - "\e[1;1H", // Move to position 1,1 - // TODO // Move pointer to top-left corner. + tmp_string, Drawings.CornerTL ); @@ -112,11 +122,9 @@ draw_box :: (x: int, y: int, width: int, height: int) { } write_string(Drawings.CornerTR); - - // TODO Take care of the temporary allocations. - for idx: 2..height-1 { - tmpL := tprint("\e[%;%H", idx, 1); - tmpR := tprint("\e[%;%H", idx, width); + 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, @@ -124,7 +132,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { Drawings.LineV); } - tmpBL := tprint("\e[%;%H", height, 1); + tmpBL := tprint(Commands.SetCursorPosition, y+height-1, x); write_strings( tmpBL, Drawings.CornerBL); @@ -133,13 +141,15 @@ draw_box :: (x: int, y: int, width: int, height: int) { } write_string(Drawings.CornerBR); - write_strings(Commands.EnterNormalMode); + write_string(Commands.EnterNormalMode); } -clear_screen :: inline () { +// TODO Maybe rename to "clear()" +clear_terminal :: inline () { write_string(Commands.ClearScreen); } +// TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { rows, columns := OS_get_terminal_size(); return rows, columns; @@ -149,41 +159,138 @@ get_terminal_size :: () -> rows: int, columns: int { // return OS_read_input(); // } -// get_cursor_position :: () -> row: int, column: int { -// assert(isTUIActive, "TUI is not active."); // TODO -// write_string(TUI.Commands.QueryCursorPosition); // Returned "\e[21;1R" -// read_input() -// } +set_cursor_position :: (row: int, column: int) { + auto_release_temp(); + tmp_string := tprint(Commands.SetCursorPosition, row, column); + write_string(tmp_string); +} +get_cursor_position :: () -> row: int, column: int { + assert(isTUIActive, "TUI is not active."); // TODO + write_string(Commands.QueryCursorPosition); // Returned + input := read_input(false); + // Result: \e[21;1R + assert( + input.data[0] == #char "\e" && + input.data[1] == #char "[" && + input.data[input.count-1] == #char "R", + "Query cursor position returned invalid response."); + + input.data += 2; + print(">%<\n", input); + parts := split(input, ";"); + row := parse_int(*parts[0]); + column := parse_int(*parts[1]); + return row, column; +} + +read_input :: (blocking: bool = true) -> string { + return OS_read_input(blocking); +} // read_input: () -> string; #if OS == .WINDOWS { - // #run read_input = () -> string { - read_input :: () -> string { + OS_read_input :: (blocking: bool) -> string { + result: string = ---; + MAX_BYTES_TO_READ :: 1024; temp : [MAX_BYTES_TO_READ] u8; - result: string = ---; bytes_read : s32; - - if !ReadConsoleA( stdin, temp.data, xx temp.count, *bytes_read ) - return ""; - - result.data = alloc(bytes_read); - result.count = bytes_read; - memcpy(result.data, temp.data, bytes_read); + + if ReadConsoleA(stdin, temp.data, xx temp.count, *bytes_read) { + result.data = alloc(bytes_read); + result.count = bytes_read; + memcpy(result.data, temp.data, bytes_read); + } return result; }; } else #if OS == .LINUX || OS == .MACOS { - #import "Basic"; - #import "POSIX"; - read_input :: () -> string { + OS_read_input :: (blocking: bool) -> string { + + term : My_Termios; + tcgetattr(STDIN_FILENO, *term); + + // Input modes. + Input_Modes :: enum 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. + } + + // IGNBRK :u32: (1 << 0); // Ignore break condition. + // BRKINT :u32: (1 << 1); // Signal interrupt on break. + // IGNPAR :u32: (1 << 2); // Ignore characters with parity errors. + // PARMRK :u32: (1 << 3); // Mark parity and framing errors. + // INPCK :u32: (1 << 4); // Enable input parity check. + // ISTRIP :u32: (1 << 5); // Strip 8th bit off characters. + // INLCR :u32: (1 << 6); // Map NL to CR on input. + // IGNCR :u32: (1 << 7); // Ignore CR. + // ICRNL :u32: (1 << 8); // Map CR to NL on input. + // IXON :u32: (1 << 9); // Enable start/stop output control. + // IXOFF :u32: (1 << 10); // Enable start/stop input control. + // IXANY :u32: (1 << 11); // Any character will restart after stop. + // IMAXBEL :u32: (1 << 13); // Ring bell when input queue is full. + // IUCLC :u32: (1 << 14); // Translate upper case input to lower case. + + // IGNBRK :u32: 0x0001; + // BRKINT :u32: 0x0002; + // IGNCR :u32: 0x0040; + + ECHO :u32: 0x0004; + ECHONL :u32: 0x0010; + ICANON :u32: 0x0080; + IEXTEN :u32: 0x0400; + + backup: My_Termios; + tcgetattr(STDIN_FILENO, *backup); + + if blocking { + term = __term; + // iflags: Input_Modes = (.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); + // term.c_iflag |= xx iflags; + // term.c_lflag |= (ECHO | ECHONL | ICANON | IEXTEN); + } + else { + iflags: Input_Modes = (.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXON); + term.c_iflag &= xx ~(iflags); + term.c_lflag &= ~(ECHO | ECHONL | ICANON | IEXTEN); + } + + // 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); - str := to_string(buffer.data, bytes_read); - return str; + write_string(Commands.HideCursor); + + // TODO WIP WIP WIP + 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 439447d..e5c81c3 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -33,7 +33,7 @@ OS_prepare_terminal :: () { 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; + term_new.c_cflag |= 0x00000030; // TODO WHAT IS THIS? tcsetattr(STDIN_FILENO, 0, *term_new); } diff --git a/ttt.jai b/ttt.jai index 0cd8ead..3910cd0 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1183,12 +1183,18 @@ main :: () { // -- -- -- TODO WIP Testing TUI -- START TUI.start(); - TUI.clear_screen(); + TUI.clear_terminal(); rows, columns := TUI.get_terminal_size(); - TUI.draw_box(1,1,columns, rows); - sleep_milliseconds(1500); + TUI.draw_box(1, 1, columns, rows); + TUI.set_cursor_position(3, 3); + input := TUI.read_input(true); + TUI.set_cursor_position(3, 3); + c_row, c_column := TUI.get_cursor_position(); + // sleep_milliseconds(1500); TUI.stop(); - print("\nr:c = %:%\n", rows, columns); + print("\ninput = %\n", input); + print("cursor r:c = %:%\n", c_row, c_column); + print("window r:c = %:%\n", rows, columns); return; -- cgit v1.2.3 From 8b640d9e236641eefea93338e3a7092678e3c5e5 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 23 Sep 2023 10:58:35 +0100 Subject: Implemented basic read_input for windows. --- TUI/module.jai | 102 ++++++++++++++++++-------------------------------------- TUI/windows.jai | 87 ++++++++++++++++++++++++++++++++++++++++++----- ttt.jai | 12 ++++--- 3 files changed, 118 insertions(+), 83 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index fae022f..3298e59 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -88,16 +88,16 @@ Commands :: struct { // TODO Maybe rename to "setup()" start :: () { - OS_prepare_terminal(); write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); + OS_prepare_terminal(); isTUIActive = true; } // TODO Maybe rename to "reset()" stop :: () { isTUIActive = false; - write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); OS_reset_terminal(); + write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } draw_box :: (x: int, y: int, width: int, height: int) { @@ -167,8 +167,11 @@ set_cursor_position :: (row: int, column: int) { get_cursor_position :: () -> row: int, column: int { assert(isTUIActive, "TUI is not active."); // TODO + + // TODO Hide input echo. + write_string(Commands.QueryCursorPosition); // Returned - input := read_input(false); + input := read_input(.MACHINE); // Result: \e[21;1R assert( input.data[0] == #char "\e" && @@ -184,32 +187,25 @@ get_cursor_position :: () -> row: int, column: int { return row, column; } -read_input :: (blocking: bool = true) -> string { - return OS_read_input(blocking); +Input_Mode :: enum u8 { + HUMAN; // Shows cursor, echoes input, and expects an enter at the end of the line. + MACHINE; // Hides cursor, hides input, and reads right away once the first input is available. } -// read_input: () -> string; - -#if OS == .WINDOWS { +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); +} - OS_read_input :: (blocking: bool) -> string { - result: string = ---; - MAX_BYTES_TO_READ :: 1024; - temp : [MAX_BYTES_TO_READ] u8; - bytes_read : s32; - if ReadConsoleA(stdin, temp.data, xx temp.count, *bytes_read) { - result.data = alloc(bytes_read); - result.count = bytes_read; - memcpy(result.data, temp.data, bytes_read); - } - return result; - }; +#if OS == .WINDOWS { + // Prototyping zone... keep clear! } else #if OS == .LINUX || OS == .MACOS { - - OS_read_input :: (blocking: bool) -> string { + // Prototyping zone... keep clear! + OS_read_input :: (mode: Input_Mode) -> string { term : My_Termios; tcgetattr(STDIN_FILENO, *term); @@ -228,7 +224,7 @@ else #if OS == .LINUX || OS == .MACOS { IXON; // Enable start/stop output control. IXOFF; // Enable start/stop input control. IXANY; // Any character will restart after stop. - __NOT_USED__; + __NOT_USED__; IMAXBEL; // Ring bell when input queue is full. IUCLC; // Translate upper case input to lower case. } @@ -253,58 +249,24 @@ else #if OS == .LINUX || OS == .MACOS { NOKERNINFO; // Disable VSTATUS. PENDIN; // Retype pending input (state). NOFLSH; // Disable flush after interrupt. - + } backup: My_Termios; tcgetattr(STDIN_FILENO, *backup); - if blocking { - // write_number(term.c_iflag, 2); - // write_string(":"); - // write_number(term.c_lflag, 2); - // write_string(":"); - // write_number(__term.c_iflag, 2); - // write_string(":"); - // write_number(__term.c_lflag, 2); - - // c_iflag - // 10 - // 8 - // 0101 0101 0000 0000 : __term - // 1111 1010 0001 0100 : &MASK - // 0101 0000 0000 0000 : - - // 0101 0101 0000 0000 : NOW - - - // c_lflag - // 15 - // 3 - // 1 - // 0 - // 1000 1010 0011 1011 : __term - // 0111 1111 1011 0100 : &MASK - // 0000 1010 0011 0000 : - - // 1000 1010 0011 1011 : NOW - - - // print("%\n", term.c_iflag, to_standard_error = true); - // print("%\n", term.c_lflag, to_standard_error = true); - // print("%\n", term, to_standard_error = true); - - // term = __term; - term.c_iflag |= xx cast(Input_Modes)(.IXOFF | .ICRNL); - term.c_lflag |= xx cast(Local_Modes)(.NOKERNINFO | .ECHO | .ECHOE | .ECHOKE); - } - else { - 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; + 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); @@ -331,7 +293,7 @@ else #if OS == .LINUX || OS == .MACOS { // 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/windows.jai b/TUI/windows.jai index ef0cfa8..24dbece 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -3,11 +3,19 @@ kernel32 :: #system_library "kernel32"; + // https://learn.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo GetConsoleScreenBufferInfo :: (hConsoleOutput: HANDLE, lpConsoleScreenBufferInfo: *CONSOLE_SCREEN_BUFFER_INFO) -> bool #foreign kernel32; - ReadConsoleA :: (hConsoleHandle: HANDLE, buff : *u8, chars_to_read : s32, chars_read : *s32, lpInputControl := *void ) -> bool #foreign kernel32; - // ReadConsole :: (hConsoleInput: HANDLE, lpBuffer: *u8, nNumberOfCharsToRead: s32, lpNumberOfCharsRead: *s32, pInputControl := *void) -> bool #foreign kernel32; + + // https://learn.microsoft.com/en-us/windows/console/readconsole + ReadConsoleA :: (hConsoleInput: HANDLE, lpBuffer: *u8, nNumberOfCharsToRead: s32, lpNumberOfCharsRead: *s32, pInputControl := *void) -> bool #foreign kernel32; + + // https://learn.microsoft.com/en-us/windows/console/getconsolemode GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *u32) -> bool #foreign kernel32; + + // https://learn.microsoft.com/en-us/windows/console/setconsolemode SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: u32) -> bool #foreign kernel32; + + // https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror GetLastError :: () -> s32 #foreign kernel32; ENABLE_VIRTUAL_TERMINAL_INPUT :: 0x0200; @@ -18,6 +26,28 @@ DISABLE_NEWLINE_AUTO_RETURN :: 0x0008; ENABLE_LVB_GRID_WORLDWIDE :: 0x0010; + + // https://learn.microsoft.com/en-us/windows/console/setconsolemode + Console_Mode :: enum_flags u32 { + _UNUSED_0001_; + 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. + _UNUSED_0008_; + 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. + _UNSED_0040_; + _UNSED_0080_; + _UNSED_0100_; + ENABLE_VIRTUAL_TERMINAL_INPUT; // + _UNSED_0400_; + _UNSED_0800_; + _UNSED_1000_; + _UNSED_2000_; + _UNSED_4000_; + _UNSED_8000_; + } + + // https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types SHORT :: s16; WORD :: u16; DWORD :: s32; @@ -46,8 +76,13 @@ stdin: HANDLE; initial_stdin_mode: u32; + default_stdin_mode: Console_Mode; + blocking_stdin_mode: Console_Mode; + unblocking_stdin_mode: Console_Mode; + stdout: HANDLE; initial_stdout_mode: u32; + default_stdout_mode: Console_Mode; OS_prepare_terminal :: () { @@ -64,14 +99,17 @@ OS_prepare_terminal :: () { print("Failed to get input mode.", to_standard_error = true); return; } - if SetConsoleMode(stdin, initial_stdin_mode | ENABLE_VIRTUAL_TERMINAL_INPUT) == false { + default_stdin_mode = (cast(Console_Mode) initial_stdin_mode) | .ENABLE_VIRTUAL_TERMINAL_INPUT; + blocking_stdin_mode = default_stdin_mode | .ENABLE_LINE_INPUT | .ENABLE_ECHO_INPUT; + unblocking_stdin_mode = default_stdin_mode & ~.ENABLE_LINE_INPUT & ~.ENABLE_ECHO_INPUT; + + if SetConsoleMode(stdin, xx default_stdin_mode) == false { print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); return; } // stdout stdout = GetStdHandle(STD_OUTPUT_HANDLE); - outMode: u32 = 0; if stdout == INVALID_HANDLE_VALUE { print("Invalid output handler.", to_standard_error = true); return; @@ -80,20 +118,19 @@ OS_prepare_terminal :: () { print("Failed to get output mode.", to_standard_error = true); return; } - if SetConsoleMode(stdout, initial_stdout_mode | ENABLE_PROCESSED_OUTPUT| ENABLE_VIRTUAL_TERMINAL_PROCESSING) == false { + default_stdout_mode = (cast(Console_Mode) initial_stdout_mode) | ENABLE_PROCESSED_OUTPUT| ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + if SetConsoleMode(stdout, xx default_stdout_mode) == false { print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); return; } } OS_reset_terminal :: () { - print("TODO TUI\n", to_standard_error = true); - if SetConsoleMode(stdin, initial_stdin_mode) == false { print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); return; } - if SetConsoleMode(stdout, initial_stdout_mode) == false { print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); return; @@ -109,3 +146,37 @@ OS_get_terminal_size :: () -> rows: int, columns: int { return rows, columns; } + +OS_read_input :: (mode: Input_Mode) -> string { + result: string = ---; + + 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. + + 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); + } + } + + 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! + + result.data = alloc(bytes_read); + result.count = bytes_read; + memcpy(result.data, temp.data, bytes_read); + } + + 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; +}; diff --git a/ttt.jai b/ttt.jai index 3910cd0..9154a60 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1186,15 +1186,17 @@ main :: () { TUI.clear_terminal(); rows, columns := TUI.get_terminal_size(); TUI.draw_box(1, 1, columns, rows); - TUI.set_cursor_position(3, 3); - input := TUI.read_input(true); - TUI.set_cursor_position(3, 3); + TUI.set_cursor_position(4, 4); c_row, c_column := TUI.get_cursor_position(); + TUI.set_cursor_position(3, 3); + input := TUI.read_input(); + // sleep_milliseconds(1500); TUI.stop(); - print("\ninput = %\n", input); - print("cursor r:c = %:%\n", c_row, c_column); + print("- - -\n"); print("window r:c = %:%\n", rows, columns); + print("cursor r:c = %:%\n", c_row, c_column); + print("input = %\n", input); return; -- cgit v1.2.3 From 32621b9b18a285dfd5e9e225073809bc49b14f39 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 28 Sep 2023 00:51:40 +0100 Subject: Working read_input prototype. --- TUI/module.jai | 170 ++++++++++++++++---------------------------------------- TUI/unix.jai | 141 +++++++++++++++++++++++++++++++++++++--------- TUI/windows.jai | 57 +++++++++---------- ttt.jai | 6 +- 4 files changed, 195 insertions(+), 179 deletions(-) (limited to 'ttt.jai') 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 + // where is the number of rows and 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;;t + // where is the number of rows and 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; +} diff --git a/ttt.jai b/ttt.jai index 9154a60..a78cf30 100644 --- a/ttt.jai +++ b/ttt.jai @@ -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); } -- cgit v1.2.3 From 3b9f4f990ab0e865ec8e6277381e5aff0b178665 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 16 Oct 2023 00:48:04 +0100 Subject: First prototype of TUI reading the input using another thread. --- TUI/module.jai | 187 ++++++++++++++++++++++++++++++++------- TUI/unix.jai | 274 +++++++++++++++++++++++++++------------------------------ ttt.jai | 151 +++++++++++++++++++++++-------- 3 files changed, 399 insertions(+), 213 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 034ec15..7a7dc54 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -8,6 +8,7 @@ #import "Basic"; #import "String"; +#import "Thread"; Drawings :: struct { CornerBR :: "\x6A"; @@ -84,18 +85,144 @@ Commands :: struct { initialized := false; +input_buffer_mutex: Mutex; +input_buffer: string; +input_counter: s64; +read_buffer: [4096] u8; + +input_process_thread: Thread; + +Key :: u8; // TODO To be improved. +Keys :: enum u8 { + None :: 0; + Resize :: 1; //410; // TODO Why?! +} + +input_semaphore: Semaphore; +key_semaphore: Semaphore; +key_mutex: Mutex; +key_input := Keys.None; +key_resize := Keys.None; +// key_queue: [2] Key; // TODO Queue with size 1!? hmmm nice! +// key_queue_idx := 0; + + + +// queue_semaphore: Semaphore; +// queue_mutex: Mutex; +// queue :: Keys; + +// Notes +// So, the semaphore usage should indicate the items on the key_queue. +// If we don't want to use a queue, maybe we can use two Key items, one for user input, and another, with higher priority, for Resize. + +dam :: (msg: string) { + print(msg, to_standard_error = true); +} + +process_input :: (thread: *Thread) -> s64 { + char: u8; + dam(">START signal_input\n"); + defer dam(">STOP signal_input\n"); + while(true) { + if initialized == false return 0; + if key_input != Keys.None { // TODO Reading without mutex... + dam("waiting.."); + sleep_milliseconds(100); + dam("!\n\r"); + continue; + } + dam("reading.."); + bytes_read := OS_read_input(*char, 1); + dam("!\n\r"); + if bytes_read > 0 { + lock(*key_mutex); + defer unlock(*key_mutex); + key_input = xx char; + dam("signaling.."); + dam("!\n\r"); + signal(*key_semaphore); + } + } + return 0; +} + +process_resize :: (signal : s32) #c_call { + new_context : Context; + push_context new_context { + if signal != SIGWINCH return; + lock(*key_mutex); + defer unlock(*key_mutex); + // TODO Only signal the key_semaphore if we don't already have a Resize on the queue. + if key_resize != Key.None continue; + key_resize = Keys.Resize; + signal(*key_semaphore); + } +} + +get_key :: (wait_milliseconds: s32) -> Key { + wait_for(*key_semaphore, wait_milliseconds); + + lock(*key_mutex); + defer unlock(*key_mutex); + + if key_resize != Keys.None { + defer key_resize = Keys.None; + return xx key_resize; + } + + defer key_input = Keys.None; + return xx key_input; +} + + start :: () { - if initialized return; - write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); + if initialized == true return; +dam("A"); + // write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); OS_prepare_terminal(); +dam("B"); + input_buffer = alloc_string(4096); + init(*input_buffer_mutex, "input_buffer_mutex"); + assert(thread_init(*input_process_thread, process_input), "Failed to initialize thread."); +dam("C"); + // init(*input_semaphore); + init(*key_semaphore); + init(*key_mutex, "key_mutex"); +dam("D"); initialized = true; + thread_start(*input_process_thread); +dam("E"); } stop :: () { if initialized == false return; initialized = false; + + // signal(*input_semaphore); // TODO Maybe use tcflush ??? + thread_deinit(*input_process_thread); + + // destroy(*input_semaphore); + destroy(*key_semaphore); + OS_reset_terminal(); - write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); + // write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); +} + +get_str :: () -> string { + lock(*input_buffer_mutex); + defer unlock(*input_buffer_mutex); + return input_buffer; + // return tprint("%", to_string(input_buffer)); + // print("%", tprint("%", input_buffer)); + // return tprint("%", input_buffer); +} + +flush_input :: () { + // TODO + lock(*input_buffer_mutex); + defer unlock(*input_buffer_mutex); + input_buffer.count = 0; } draw_box :: (x: int, y: int, width: int, height: int) { @@ -191,33 +318,33 @@ Input_Mode :: enum u8 { MACHINE; // Hides cursor, hides input, and reads right away once the first input is available. } -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); -} +// 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 { diff --git a/TUI/unix.jai b/TUI/unix.jai index 434935b..1bfa400 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -6,175 +6,158 @@ // Required to do unlocking input. libc :: #system_library "libc"; + // TODO Remote this. + cfmakeraw :: (termios: *Terminal_IO_Mode) -> void #foreign 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; + 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; + tcgetattr :: (fd: s32, termios_p: *Terminal_IO_Mode) -> s32 #foreign libc; + + // 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. - // https://elixir.bootlin.com/glibc/latest/source/bits/termios.h 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. - - _1000_; - IMAXBEL; // Ring bell when input queue is full. - IUCLC; // Translate upper case input to lower case. not in POSIX - _8000_; + 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. - // https://elixir.bootlin.com/glibc/latest/source/bits/termios.h Output_Modes :: enum_flags u32 { - OPOST; // Perform output processing. - ONLCR; // Map NL to CR-NL on output. - _UNUSED_0004_; - ONOEOT; // Discard EOT (^D) on output. - OCRNL; // Map CR to NL. - ONOCR; // Discard CR's when on column 0. - ONLRET; // Move to column 0 on NL. - _UNUSED_0080_; + 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. - // https://elixir.bootlin.com/glibc/latest/source/bits/termios.h - Control_Modes :: enum_flags u32 { - CIGNORE; // Ignore these control flags. - _0002_; - _0004_; - _0008_; - - _0010_; - _0020_; - _0040_; - _0080_; - - _0100_; - _0200_; - CSTOPB; // Two stop bits instead of one. - CREAD; // Enable receiver. - - PARENB; // Parity enable. - PARODD; // Odd parity instead of even. - HUPCL; // Hang up on last close. - CLOCAL; // Ignore modem status lines. - - CRTSCTS; // RTS/CTS flow control. - CDTRCTS; // DTR/CTS flow control. - _0004_0000_; - _0008_0000_; - - MDMBUF; // DTR/DCD flow control. + 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; } - - CHWFLOW :Control_Modes : (.MDMBUF | .CRTSCTS | .CDTRCTS); // All types of flow control. - CS5 :Control_Modes : 0x0000; // 5 bits per byte. - CS6 :Control_Modes : 0x0100; // 6 bits per byte. - CS7 :Control_Modes : 0x0200; // 7 bits per byte. - CS8 :Control_Modes : CS6|CS7; // 8 bits per byte. - CSIZE :Control_Modes : CS5|CS6|CS7|CS8; // Number of bits per byte (mask). - + // Local modes. - // https://elixir.bootlin.com/glibc/latest/source/bits/termios.h 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. - - _1000_; - _2000_; - _4000_; - _8000_; - - _0001_0000_; - _0002_0000_; - // TODO Maybe reduce the amount of unused flags by setting the value when there are holes on the table. - // TOSTOP :: 0x0004_0000; // Send SIGTTOU for background output. - TOSTOP; // Send SIGTTOU for background output. - FLUSHO; // Output being flushed (state). - - XCASE; // Canonical upper/lower case. - NOKERNINFO; // Disable VSTATUS. - _0040_0000_; - _0080_0000_; - - _0100_0000_; - PENDIN; // Retype pending input (state). - _0400_0000_; - NOFLSH; // Disable flush after interrupt. + 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. } - 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]u8; // Control characters. - c_ispeed : u32; // Input speed (baud rates). - c_ospeed : u32; // Output speed (baud rates). + // 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; - default_tio_mode: Terminal_IO_Mode; // TODO Rename to 'raw_tio_mode' - human_tio_mode: Terminal_IO_Mode; // TODO Rename to 'cooked_tio_mode' + raw_tio_mode: Terminal_IO_Mode; #scope_export OS_prepare_terminal :: () { - tcgetattr(STDIN_FILENO, *initial_tio_mode); - - default_tio_mode = initial_tio_mode; - default_tio_mode.c_iflag &= ~(.IGNBRK | .BRKINT | .PARMRK | .ISTRIP | .INLCR | .IGNCR | .ICRNL | .IXOFF); - default_tio_mode.c_oflag &= ~(.OPOST); - default_tio_mode.c_cflag &= ~(CSIZE | .PARENB); - default_tio_mode.c_lflag &= ~(.ECHOCTL |.ECHO | .ECHOE | .ECHOKE); - - human_tio_mode = default_tio_mode; - human_tio_mode.c_iflag |= (.IXOFF | .ICRNL); - human_tio_mode.c_lflag |= (.NOKERNINFO | .ECHO | .ECHOE | .ECHOKE); - - tcsetattr(STDIN_FILENO, 0, *default_tio_mode); + error: s32; + tcgetattr(STDIN_FILENO, *initial_tio_mode); // TODO Log on error. + 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] = 0; + raw_tio_mode.c_cc[Control_Chars.VTIME] = 1; + tcsetattr(STDIN_FILENO, 0, *raw_tio_mode); // TODO Log on error. } OS_reset_terminal :: () { - tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); + tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); // TODO Log on error. } OS_get_terminal_size :: () -> rows: int, columns: int { auto_release_temp(); - input := talloc_string(64); + flush_input(); + + // input := talloc_string(64); write_string(Commands.QueryWindowSizeInChars); - input.count = OS_read_input(input.data, input.count); + // input.count = OS_read_input(input.data, input.count); + sleep_milliseconds(1); //TODO WHYYYY??? GOD DAMIT THREADS... + input := get_str(); + // input[0] = #char "x"; + // print(">%<", xx input); + // print("#<#"); // Expected message format: [8;;t // where is the number of rows and of columns. @@ -182,6 +165,7 @@ OS_get_terminal_size :: () -> rows: int, columns: int { input[0] == #char "\e" && input[1] == #char "[" && input[2] == #char "8", + //input[input.count-1] == #char "t", "Query windows size in chars returned invalid response."); parts := split(input, ";"); @@ -190,17 +174,17 @@ OS_get_terminal_size :: () -> rows: int, columns: int { return rows, columns; } -OS_set_input_mode :: (mode: Input_Mode) { - tio_mode: *Terminal_IO_Mode = ---; - if mode == { - case .HUMAN; - tio_mode = *human_tio_mode; - case; - tio_mode = *default_tio_mode; - } - tcsetattr(STDIN_FILENO, 0, tio_mode); - // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) -} +// OS_set_input_mode :: (mode: Input_Mode) { + // tio_mode: *Terminal_IO_Mode = ---; + // if mode == { + // case .HUMAN; + // tio_mode = *human_tio_mode; + // case; + // tio_mode = *cooked_tio_mode; + // } + // tcsetattr(STDIN_FILENO, 0, tio_mode); + // // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) +// } 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); diff --git a/ttt.jai b/ttt.jai index a78cf30..0515262 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1181,50 +1181,125 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo main :: () { - // -- -- -- TODO WIP Testing TUI -- START - TUI.start(); - TUI.clear_terminal(); - rows, columns := TUI.get_terminal_size(); - TUI.draw_box(1, 1, columns, rows); - TUI.set_cursor_position(4, 4); - c_row, c_column := TUI.get_cursor_position(); - TUI.set_cursor_position(3, 3); - input := TUI.read_input(); + #if 0 { + print("test 0\n"); + TUI.test(); + return; + } - // sleep_milliseconds(5000); - TUI.stop(); - print("- - -\n"); - print("window r:c = %:%\n", rows, columns); - print("cursor r:c = %:%\n", c_row, c_column); - print("input = %\n", input); - return; + #if 0 { + print("test 1\n"); + TUI.start(); + buffer: [8] u8; + brss, error, msg := TUI.OS_read_input(buffer.data, buffer.count); + TUI.stop(); + if error == true print("error:%", msg); + print("br:%", brss); + print("input:%", cast(string)buffer); + TUI.stop(); + return; + } + #if 0 { + print("test 2\n"); + key: TUI.Key = #char "d"; + while(key != #char "q") { + // key = TUI.get_key(10); + buffer: [8] u8; + buffer[0] = 0; + brss := TUI.OS_read_input(buffer.data, 1); + if brss > 0 { + key = buffer[0]; + print(">%<", xx key); + } + else { + print(":"); + } + sleep_milliseconds(10); + } + TUI.stop(); + return; + } - str: string; + #if 1 { + print("test 3\n"); + TUI.start(); + key: TUI.Key = #char "d"; + while(key != #char "q") { + __mark := get_temporary_storage_mark(); + key = TUI.get_key(3000); + // sleep_milliseconds(10); + + if key != xx TUI.Keys.None print_character(key); + else write_string("-"); + set_temporary_storage_mark(__mark); + } + TUI.stop(); + return; + } + + // TUI.start_new_mode(); + // sleep_milliseconds(3000); + xrows, xcolumns := TUI.get_terminal_size(); + TUI.draw_box(1, 1, xcolumns, xrows); + // print(""); + bbb: [64] u8; + + br, err, err_msg := TUI.OS_read_input(bbb.data, 5); + print(">%/%/%/%\n", cast(string)bbb, br, err, err_msg); + sleep_milliseconds(3000); + // coisa := TUI.get_str(); + // print("\n>%<\n", coisa); + print("stopping\n"); + // TUI.stop_new_mode(); + TUI.stop(); + return; - write_string("\e(0"); // Enter Line drawing mode - write_string("\e[104;93m"); // bright yellow on bright blue - write_string("x"); // in line drawing mode, \x78 -> \u2502 "Vertical Bar" - write_string("\e[10m"); // restore color - write_string("\e(B"); // exit line drawing mode - sleep_milliseconds(1000); + // -- -- -- TODO WIP Testing TUI -- START + // TUI.start(); + // TUI.clear_terminal(); + // rows, columns := TUI.get_terminal_size(); + // TUI.draw_box(1, 1, columns, rows); + // TUI.set_cursor_position(4, 4); + // c_row, c_column := TUI.get_cursor_position(); + // TUI.set_cursor_position(3, 3); + // input := TUI.read_input(); - // str = "\e[sWOW\e[u\e[1P"; - str = "\e[sWOW"; - // write(STDIN_FILENO, str.data, xx str.count); - write_string(str); - sleep_milliseconds(1000); - - str = "\e[?25l"; - // write(STDIN_FILENO, str.data, xx str.count); - write_string(str); - sleep_milliseconds(1000); - - str = "\e[?25h"; - // write(STDIN_FILENO, str.data, xx str.count); - write_string(str); - return; + // sleep_milliseconds(5000); + // TUI.stop(); + // print("- - -\n"); + // print("window r:c = %:%\n", rows, columns); + // print("cursor r:c = %:%\n", c_row, c_column); + // print("input = %\n", input); + // return; +// +// + // str: string; +// + // write_string("\e(0"); // Enter Line drawing mode + // write_string("\e[104;93m"); // bright yellow on bright blue + // write_string("x"); // in line drawing mode, \x78 -> \u2502 "Vertical Bar" + // write_string("\e[10m"); // restore color + // write_string("\e(B"); // exit line drawing mode + // sleep_milliseconds(1000); + + + // // str = "\e[sWOW\e[u\e[1P"; + // str = "\e[sWOW"; + // // write(STDIN_FILENO, str.data, xx str.count); + // write_string(str); + // sleep_milliseconds(1000); + // + // str = "\e[?25l"; + // // write(STDIN_FILENO, str.data, xx str.count); + // write_string(str); + // sleep_milliseconds(1000); + // + // str = "\e[?25h"; + // // write(STDIN_FILENO, str.data, xx str.count); + // write_string(str); + // return; // -- -- -- TODO WIP Testing TUI -- STOP -- cgit v1.2.3 From bdaba41cbe7f10c750c31b66dd2696e6b6ea7436 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 16 Oct 2023 01:23:40 +0100 Subject: Improved input reading to wait for event instead of pooling. --- TUI/module.jai | 19 +++---------------- TUI/unix.jai | 4 ++-- ttt.jai | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 7a7dc54..c1f40ce 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -98,19 +98,10 @@ Keys :: enum u8 { Resize :: 1; //410; // TODO Why?! } -input_semaphore: Semaphore; key_semaphore: Semaphore; key_mutex: Mutex; key_input := Keys.None; key_resize := Keys.None; -// key_queue: [2] Key; // TODO Queue with size 1!? hmmm nice! -// key_queue_idx := 0; - - - -// queue_semaphore: Semaphore; -// queue_mutex: Mutex; -// queue :: Keys; // Notes // So, the semaphore usage should indicate the items on the key_queue. @@ -126,9 +117,9 @@ process_input :: (thread: *Thread) -> s64 { defer dam(">STOP signal_input\n"); while(true) { if initialized == false return 0; - if key_input != Keys.None { // TODO Reading without mutex... + if key_input != Keys.None { dam("waiting.."); - sleep_milliseconds(100); + sleep_milliseconds(100); // We could increase this value as a push-back mechanism if we're just hitting it repeateadly. dam("!\n\r"); continue; } @@ -170,7 +161,7 @@ get_key :: (wait_milliseconds: s32) -> Key { defer key_resize = Keys.None; return xx key_resize; } - + defer key_input = Keys.None; return xx key_input; } @@ -186,7 +177,6 @@ dam("B"); init(*input_buffer_mutex, "input_buffer_mutex"); assert(thread_init(*input_process_thread, process_input), "Failed to initialize thread."); dam("C"); - // init(*input_semaphore); init(*key_semaphore); init(*key_mutex, "key_mutex"); dam("D"); @@ -199,10 +189,7 @@ stop :: () { if initialized == false return; initialized = false; - // signal(*input_semaphore); // TODO Maybe use tcflush ??? thread_deinit(*input_process_thread); - - // destroy(*input_semaphore); destroy(*key_semaphore); OS_reset_terminal(); diff --git a/TUI/unix.jai b/TUI/unix.jai index 1bfa400..523aeb5 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -135,8 +135,8 @@ OS_prepare_terminal :: () { 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] = 0; - raw_tio_mode.c_cc[Control_Chars.VTIME] = 1; + 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. } diff --git a/ttt.jai b/ttt.jai index 0515262..c834bf1 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1227,8 +1227,8 @@ main :: () { key: TUI.Key = #char "d"; while(key != #char "q") { __mark := get_temporary_storage_mark(); + sleep_milliseconds(1000); key = TUI.get_key(3000); - // sleep_milliseconds(10); if key != xx TUI.Keys.None print_character(key); else write_string("-"); -- cgit v1.2.3 From 70d3b82ca7817a066120ff88deb8b56e90d454ca Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 24 Oct 2023 21:27:58 +0100 Subject: Prototype of process_input that does not block reading. --- TUI/module.jai | 30 ++++++++++++++++++++---------- TUI/unix.jai | 4 ++-- ttt.jai | 7 ++++++- 3 files changed, 28 insertions(+), 13 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index f113c2a..6854d59 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -102,6 +102,7 @@ key_semaphore: Semaphore; key_mutex: Mutex; key_input := Keys.None; key_resize := Keys.None; +key_buffer: [64] Keys; // Notes // So, the semaphore usage should indicate the items on the key_queue. @@ -111,8 +112,11 @@ dam :: (msg: string) { print(msg, to_standard_error = true); } + process_input :: (thread: *Thread) -> s64 { - char: u8; + buffer: [64] u8; + input: string; + // char: u8; dam(">START signal_input\n"); defer dam(">STOP signal_input\n"); while(true) { @@ -123,17 +127,20 @@ process_input :: (thread: *Thread) -> s64 { dam("!\n\r"); continue; } - dam("reading.."); - bytes_read := OS_read_input(*char, 1); - dam("!\n\r"); - if bytes_read > 0 { + if input.count > 0 { lock(*key_mutex); - defer unlock(*key_mutex); - key_input = xx char; - dam("signaling.."); - dam("!\n\r"); + key_input = xx input[0]; + unlock(*key_mutex); signal(*key_semaphore); + + advance(*input); + continue; } + + // dam("reading.."); + bytes_read := OS_read_input(buffer.data, buffer.count); + input.data = buffer.data; + input.count = bytes_read; } return 0; } @@ -213,9 +220,12 @@ stop :: () { restore_handler(); // TODO Move to beter place. + print(">A<"); thread_deinit(*input_process_thread); + print(">B<"); destroy(*key_semaphore); - + + print(">C<"); OS_reset_terminal(); write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } diff --git a/TUI/unix.jai b/TUI/unix.jai index 523aeb5..1bfa400 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -135,8 +135,8 @@ OS_prepare_terminal :: () { 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; + raw_tio_mode.c_cc[Control_Chars.VMIN] = 0; + raw_tio_mode.c_cc[Control_Chars.VTIME] = 1; tcsetattr(STDIN_FILENO, 0, *raw_tio_mode); // TODO Log on error. } diff --git a/ttt.jai b/ttt.jai index c834bf1..8edc8b4 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1227,14 +1227,19 @@ main :: () { key: TUI.Key = #char "d"; while(key != #char "q") { __mark := get_temporary_storage_mark(); - sleep_milliseconds(1000); + // sleep_milliseconds(1000); key = TUI.get_key(3000); if key != xx TUI.Keys.None print_character(key); else write_string("-"); set_temporary_storage_mark(__mark); } + print("Waiting 1s.."); + sleep_milliseconds(1000); + print("!\n\r"); + print("Stoping.."); TUI.stop(); + print("!\n\r"); return; } -- cgit v1.2.3 From 90beac697609d7d4926e18a0eb0c576b9a22058b Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 Nov 2023 03:51:35 +0000 Subject: Reading input was greatly simplified. --- TUI/module.jai | 280 ++++++++++++++++++++++++++------------------------------- TUI/unix.jai | 56 +++--------- sizeof.c | 35 ++++---- ttt.jai | 58 ++++++++---- 4 files changed, 198 insertions(+), 231 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 6854d59..6ca6f59 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -84,13 +84,8 @@ Commands :: struct { initialized := false; - -input_buffer_mutex: Mutex; -input_buffer: string; -input_counter: s64; -read_buffer: [4096] u8; - -input_process_thread: Thread; +input_buffer : [64] u8; +input_string : string; Key :: u8; // TODO To be improved. Keys :: enum u8 { @@ -98,152 +93,94 @@ Keys :: enum u8 { Resize :: 1; //410; // TODO Why?! } -key_semaphore: Semaphore; -key_mutex: Mutex; -key_input := Keys.None; -key_resize := Keys.None; -key_buffer: [64] Keys; - -// Notes -// So, the semaphore usage should indicate the items on the key_queue. -// If we don't want to use a queue, maybe we can use two Key items, one for user input, and another, with higher priority, for Resize. dam :: (msg: string) { print(msg, to_standard_error = true); } - -process_input :: (thread: *Thread) -> s64 { - buffer: [64] u8; - input: string; - // char: u8; - dam(">START signal_input\n"); - defer dam(">STOP signal_input\n"); - while(true) { - if initialized == false return 0; - if key_input != Keys.None { - dam("waiting.."); - sleep_milliseconds(100); // We could increase this value as a push-back mechanism if we're just hitting it repeateadly. - dam("!\n\r"); - continue; - } - if input.count > 0 { - lock(*key_mutex); - key_input = xx input[0]; - unlock(*key_mutex); - signal(*key_semaphore); - - advance(*input); - continue; - } - - // dam("reading.."); - bytes_read := OS_read_input(buffer.data, buffer.count); - input.data = buffer.data; - input.count = bytes_read; - } - return 0; +push_key :: (key: Key) { + // TODO } -process_resize :: (signal_code : s32) #c_call { - new_context : Context; - push_context new_context { - print("SIGNAL:%", signal_code); - if signal_code != SIGWINCH return; - print("RESIZE\n"); - lock(*key_mutex); - defer unlock(*key_mutex); - // TODO Only signal the key_semaphore if we don't already have a Resize on the queue. - if key_resize != Keys.None return; - key_resize = Keys.Resize; - signal(*key_semaphore); - } -} +get_key :: (timeout_milliseconds: s32 = -1) -> Key { -get_key :: (wait_milliseconds: s32) -> Key { - wait_for(*key_semaphore, wait_milliseconds); - - lock(*key_mutex); - defer unlock(*key_mutex); + if OS_was_terminal_resized() return xx Keys.Resize; - if key_resize != Keys.None { - defer key_resize = Keys.None; - return xx key_resize; + if input_string.count > 0 { + defer advance(*input_string, 1); + return input_string[0]; } - defer key_input = Keys.None; - return xx key_input; -} + is_input_available := OS_wait_for_input(timeout_milliseconds); + if OS_was_terminal_resized() return xx Keys.Resize; -// TODO Rename this procedure. -set_handler :: () { - sa : sigaction_t; - sa.sa_handler = process_resize; - sigemptyset(*(sa.sa_mask)); - sa.sa_flags = SA_RESTART; - sigaction(SIGWINCH, *sa, null); + if is_input_available { + bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. + if bytes_read > 0 { + input_string.data = input_buffer.data; + input_string.count = bytes_read; + defer advance(*input_string, 1); + return input_string[0]; + } + } + return xx Keys.None; } -// TODO Rename this procedure. -restore_handler :: () { - sa : sigaction_t; - sa.sa_handler = SIG_DFL; - sigaction(SIGWINCH, null, *sa); +get_str :: (str_limit: int = -1, allocator: Allocator = temp) -> string { + assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); + + if str_limit < 0 { + 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 < buffer.allocated break; + expand(*builder); + } + return builder_to_string(*builder, allocator); + } + else { + buffer := alloc_string(str_limit, allocator); + buffer.count = OS_read_input(buffer.data, str_limit); + return buffer; + } } start :: () { if initialized == true return; -dam("A"); - // write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); + + write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); OS_prepare_terminal(); -dam("B"); - input_buffer = alloc_string(4096); - init(*input_buffer_mutex, "input_buffer_mutex"); - assert(thread_init(*input_process_thread, process_input), "Failed to initialize thread."); -dam("C"); - init(*key_semaphore); - init(*key_mutex, "key_mutex"); -dam("D"); + + was_resized = false; + init(*resize_mutex, "resize_mutex"); + input_string.data = input_buffer.data; + input_string.count = 0; + initialized = true; - thread_start(*input_process_thread); + set_handler(); // TODO Move to beter place. -dam("E"); - } stop :: () { if initialized == false return; initialized = false; - restore_handler(); // TODO Move to beter place. - print(">A<"); - thread_deinit(*input_process_thread); - print(">B<"); - destroy(*key_semaphore); - print(">C<"); OS_reset_terminal(); write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } -get_str :: () -> string { - lock(*input_buffer_mutex); - defer unlock(*input_buffer_mutex); - return input_buffer; - // return tprint("%", to_string(input_buffer)); - // print("%", tprint("%", input_buffer)); - // return tprint("%", input_buffer); -} - flush_input :: () { // TODO - lock(*input_buffer_mutex); - defer unlock(*input_buffer_mutex); - input_buffer.count = 0; } draw_box :: (x: int, y: int, width: int, height: int) { @@ -299,7 +236,31 @@ clear_terminal :: inline () { // 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(); + rows, columns: int = ---; + #if OS == .WINDOWS { + rows, columns = OS_get_terminal_size(); + } + else { + auto_release_temp(); + + flush_input(); + write_string(Commands.QueryWindowSizeInChars); + + input := get_str(64); + + // Expected message format: [8;;t + // where is the number of rows and of columns. + assert( + input[0] == #char "\e" && + input[1] == #char "[" && + input[2] == #char "8", + //input[input.count-1] == #char "t", + "Query window size in chars returned invalid response."); + + parts := split(input, ";"); + rows = parse_int(*parts[1]); + columns = parse_int(*parts[2]); + } return rows, columns; } @@ -313,11 +274,11 @@ get_cursor_position :: () -> row: int, column: int { assert(initialized, "TUI is not ready."); // TODO Should I use this inside each and every procedure? auto_release_temp(); - + + flush_input(); write_string(Commands.QueryCursorPosition); - input := talloc_string(64); - input.count = OS_read_input(input.data, input.count); // TODO Does not check for read errors. + input := get_str(64); // Expected message format: \e[;R // where is the number of rows and of columns. @@ -334,43 +295,58 @@ get_cursor_position :: () -> row: int, column: int { return row, column; } -Input_Mode :: enum u8 { - HUMAN; // Shows cursor, echoes input, and expects an enter at the end of the line. - MACHINE; // Hides cursor, hides input, and reads right away once the first input is available. -} - -// 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 { // Prototyping zone... keep clear! } else #if OS == .LINUX || OS == .MACOS { // Prototyping zone... keep clear! + + OS_wait_for_input :: (timeout_milliseconds: s32) -> 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(); + return ifx poll_return > 0 then true else false; + } + + resize_mutex : Mutex; + was_resized : bool; + + OS_was_terminal_resized :: () -> bool { + lock(*resize_mutex); + defer unlock(*resize_mutex); + defer was_resized = false; + return was_resized; + } + + process_resize :: (signal_code : s32) #c_call { + new_context : Context; + push_context new_context { + print("SIGNAL:%", signal_code); + if signal_code != SIGWINCH then return; + if was_resized == true then return; + print("RESIZE\n"); + lock(*resize_mutex); + defer unlock(*resize_mutex); + was_resized = true; + } + } + + // TODO Rename this procedure. + set_handler :: () { + sa : sigaction_t; + sa.sa_handler = process_resize; + sigemptyset(*(sa.sa_mask)); + sa.sa_flags = SA_RESTART; + sigaction(SIGWINCH, *sa, null); + } + + // TODO Rename this procedure. + restore_handler :: () { + sa : sigaction_t; + sa.sa_handler = SIG_DFL; + sigaction(SIGWINCH, null, *sa); + } + } diff --git a/TUI/unix.jai b/TUI/unix.jai index 1bfa400..a35a60a 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -6,6 +6,16 @@ // Required to do unlocking input. libc :: #system_library "libc"; + + // int poll(struct pollfd *fds, nfds_t nfds, int timeout); + pollfd :: struct { + fd : s32; // File descriptor. + events : s16; // Requested events. + revents : s16; // Returned events. + }; + + poll :: (fds: *pollfd, nfds: u32, timeout: s32) -> s32 #foreign libc; + // TODO Remote this. cfmakeraw :: (termios: *Terminal_IO_Mode) -> void #foreign libc; @@ -135,8 +145,8 @@ OS_prepare_terminal :: () { 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] = 0; - raw_tio_mode.c_cc[Control_Chars.VTIME] = 1; + 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. } @@ -144,48 +154,6 @@ OS_reset_terminal :: () { tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); // TODO Log on error. } -OS_get_terminal_size :: () -> rows: int, columns: int { - - auto_release_temp(); - - flush_input(); - - // input := talloc_string(64); - write_string(Commands.QueryWindowSizeInChars); - // input.count = OS_read_input(input.data, input.count); - sleep_milliseconds(1); //TODO WHYYYY??? GOD DAMIT THREADS... - input := get_str(); - // input[0] = #char "x"; - // print(">%<", xx input); - // print("#<#"); - - // Expected message format: [8;;t - // where is the number of rows and of columns. - assert( - input[0] == #char "\e" && - input[1] == #char "[" && - input[2] == #char "8", - //input[input.count-1] == #char "t", - "Query windows size in chars returned invalid response."); - - parts := split(input, ";"); - rows := parse_int(*parts[1]); - columns := parse_int(*parts[2]); - return rows, columns; -} - -// OS_set_input_mode :: (mode: Input_Mode) { - // tio_mode: *Terminal_IO_Mode = ---; - // if mode == { - // case .HUMAN; - // tio_mode = *human_tio_mode; - // case; - // tio_mode = *cooked_tio_mode; - // } - // tcsetattr(STDIN_FILENO, 0, tio_mode); - // // TODO get_error_value_and_string :: () -> (error_code: OS_Error_Code, description: string) -// } - 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 { diff --git a/sizeof.c b/sizeof.c index 9025f78..c260c31 100644 --- a/sizeof.c +++ b/sizeof.c @@ -18,32 +18,33 @@ #include int main(int argc, char **argv) { - initscr(); + // initscr(); fprintf(stderr, "sizeof char: %d\n", sizeof(char)); fprintf(stderr, "sizeof short: %d\n", sizeof(short)); fprintf(stderr, "sizeof int: %d\n", sizeof(int)); + fprintf(stderr, "sizeof long: %d\n", sizeof(long)); fprintf(stderr, "sizeof unsigned: %d\n", sizeof(unsigned)); fprintf(stderr, "sizeof chtype: %d\n", sizeof(chtype)); - int w_size_x, w_size_y; - getmaxyx(stdscr, w_size_y, w_size_x); - char str[64]; - memset(str, 0, 64); - sprintf(str, "x,y : %dx%d\n", w_size_x, w_size_y); - mvaddstr(2, 2, str); - sprintf(str, "resize:%d\n", KEY_RESIZE); - mvaddstr(3, 2, str); + // int w_size_x, w_size_y; + // getmaxyx(stdscr, w_size_y, w_size_x); + // char str[64]; + // memset(str, 0, 64); + // sprintf(str, "x,y : %dx%d\n", w_size_x, w_size_y); + // mvaddstr(2, 2, str); + // sprintf(str, "resize:%d\n", KEY_RESIZE); + // mvaddstr(3, 2, str); - unsigned m = ACS_DIAMOND; + // unsigned m = ACS_DIAMOND; fprintf(stderr, "sizeof ACS %d\n", sizeof(ACS_DIAMOND)); - - if (ACS_DIAMOND != 0 || ACS_URCORNER != 0){ - fprintf(stderr, "BAZINGA\n"); - } + // + // if (ACS_DIAMOND != 0 || ACS_URCORNER != 0){ + // fprintf(stderr, "BAZINGA\n"); + // } // fprintf(stderr, ">%d<\n", strlen(ACS_DIAMOND)); - mvaddch(0, 0, m); - getch(); - endwin(); + // mvaddch(0, 0, m); + // getch(); + // endwin(); } diff --git a/ttt.jai b/ttt.jai index 8edc8b4..9ab3db6 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1221,7 +1221,17 @@ main :: () { return; } - #if 1 { + #if 0 { + print("testing select\n"); + TUI.start(); + TUI.test(); + TUI.test(); + TUI.test(); + TUI.stop(); + return; + } + + #if 0 { print("test 3\n"); TUI.start(); key: TUI.Key = #char "d"; @@ -1242,23 +1252,35 @@ main :: () { print("!\n\r"); return; } - - // TUI.start_new_mode(); - // sleep_milliseconds(3000); - xrows, xcolumns := TUI.get_terminal_size(); - TUI.draw_box(1, 1, xcolumns, xrows); - // print(""); - bbb: [64] u8; - - br, err, err_msg := TUI.OS_read_input(bbb.data, 5); - print(">%/%/%/%\n", cast(string)bbb, br, err, err_msg); - sleep_milliseconds(3000); - // coisa := TUI.get_str(); - // print("\n>%<\n", coisa); - print("stopping\n"); - // TUI.stop_new_mode(); - TUI.stop(); - return; + + #if 1 { + print("test 4\n", to_standard_error = true); + TUI.start(); + xcolumns, xrows: int; + key: TUI.Key = #char "d"; + while(key != #char "q") { + __mark := get_temporary_storage_mark(); + TUI.set_cursor_position(3, 3); + write_string("dam "); + print("%:%", xcolumns, xrows); + key = TUI.get_key(3000); + if key == xx TUI.Keys.None { + write_string(">bazinga<"); + sleep_milliseconds(1000); + } + else if key == xx TUI.Keys.Resize { + TUI.clear_terminal(); + xrows, xcolumns = TUI.get_terminal_size(); + TUI.draw_box(1, 1, xcolumns, xrows); + } + else { + print_character(key); + } + } + TUI.stop(); + print("size(CxR): %x%\n", xcolumns, xrows); + return; + } // -- -- -- TODO WIP Testing TUI -- START -- cgit v1.2.3 From 52ae0bb03c0dec13875d05b68e0f7912e5df8f23 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 21 Nov 2023 01:59:04 +0000 Subject: Cleanup get_str procedure and TUI/unix. --- TUI/module.jai | 42 +++++++++++++++++++++---------------- TUI/unix.jai | 66 ++++++++++++++++++++++++++++++---------------------------- ttt.jai | 14 +++++++++++-- 3 files changed, 70 insertions(+), 52 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 18df2c5..332f7c5 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -86,24 +86,30 @@ initialized := false; input_buffer : [64] u8; input_string : string; +input_override : Key; -Key :: u8; // TODO To be improved. -Keys :: enum u8 { - None :: 0; - Resize :: 1; //410; // TODO Why?! -} +// TODO WIP WIP WIP +// >>>>>>>>>>>>>>>>> Decide what we'll use as Key... and start fixing the types used on the procedures. +; +Key :: u32; -dam :: (msg: string) { - print(msg, to_standard_error = true); +Keys :: enum Key { + None :: 0; + Resize :: 0xFF000001; // TODO Using a value incompatible with UTF-8. } -push_key :: (key: Key) { - // TODO +set_next_key :: (key: Key) { + input_override = key; } get_key :: (timeout_milliseconds: s32 = -1) -> Key { + if input_override != xx Keys.None { + defer input_override = xx Keys.None; + return input_override; + } + if OS_was_terminal_resized() return xx Keys.Resize; if input_string.count > 0 { @@ -127,10 +133,10 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return xx Keys.None; } -get_str :: (str_limit: int = -1, allocator: Allocator = temp) -> string { +get_str :: (count_limit: int = -1, allocator: Allocator = temp) -> string { assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); - - if str_limit < 0 { + + if count_limit < 0 { builder: String_Builder(); builder.allocator = allocator; init_string_builder(*builder); @@ -145,22 +151,21 @@ get_str :: (str_limit: int = -1, allocator: Allocator = temp) -> string { return builder_to_string(*builder, allocator); } else { - buffer := alloc_string(str_limit, allocator); - buffer.count = OS_read_input(buffer.data, str_limit); + buffer := alloc_string(count_limit, allocator); + buffer.count = OS_read_input(buffer.data, count_limit); return buffer; } } - - start :: () { if initialized == true return; write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); OS_prepare_terminal(); - input_string.data = input_buffer.data; - input_string.count = 0; + input_string.data = input_buffer.data; + input_string.count = 0; + input_override = xx Keys.None; initialized = true; } @@ -259,6 +264,7 @@ get_terminal_size :: () -> rows: int, columns: int { } set_cursor_position :: (row: int, column: int) { + assert(initialized, "TUI is not ready."); auto_release_temp(); tmp_string := tprint(Commands.SetCursorPosition, row, column); write_string(tmp_string); diff --git a/TUI/unix.jai b/TUI/unix.jai index 7ff1e72..fb79f86 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -133,6 +133,39 @@ initial_tio_mode: Terminal_IO_Mode; raw_tio_mode: Terminal_IO_Mode; +//////////////////////////////////////////////////////////////////////////////// +// Resize detection +resize_mutex : Mutex; +was_resized : bool; + +resize_handler :: (signal_code : s32) #c_call { + new_context : Context; + push_context new_context { + print("SIGNAL:%", signal_code); + if signal_code != SIGWINCH then return; + if was_resized == true then return; + print("RESIZE\n"); + lock(*resize_mutex); + defer unlock(*resize_mutex); + 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 @@ -154,9 +187,8 @@ OS_prepare_terminal :: () { } OS_reset_terminal :: () { - tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); // TODO Log on error. - restore_resize_handler(); + tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); // TODO Log on error. } OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bool = false, error_message: string = "" { @@ -176,39 +208,9 @@ OS_wait_for_input :: (timeout_milliseconds: s32) -> is_input_available: bool { return ifx poll_return > 0 then true else false; } -resize_mutex : Mutex; -was_resized : bool; - OS_was_terminal_resized :: () -> bool { lock(*resize_mutex); defer unlock(*resize_mutex); defer was_resized = false; return was_resized; } - -resize_handler :: (signal_code : s32) #c_call { - new_context : Context; - push_context new_context { - print("SIGNAL:%", signal_code); - if signal_code != SIGWINCH then return; - if was_resized == true then return; - print("RESIZE\n"); - lock(*resize_mutex); - defer unlock(*resize_mutex); - 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); -} diff --git a/ttt.jai b/ttt.jai index 9ab3db6..110d089 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1259,7 +1259,8 @@ main :: () { xcolumns, xrows: int; key: TUI.Key = #char "d"; while(key != #char "q") { - __mark := get_temporary_storage_mark(); + // __mark := get_temporary_storage_mark(); + TUI.set_cursor_position(3, 3); write_string("dam "); print("%:%", xcolumns, xrows); @@ -1273,8 +1274,17 @@ main :: () { xrows, xcolumns = TUI.get_terminal_size(); TUI.draw_box(1, 1, xcolumns, xrows); } + else if key == #char "i" { + auto_release_temp(); + TUI.set_cursor_position(7, 3); + write_string("input: "); + TUI.flush_input(); + str := TUI.get_str(3); + print(">%<\n\r", str); + TUI.set_cursor_position(8, 3); + } else { - print_character(key); + print_character(cast(u8)key); } } TUI.stop(); -- cgit v1.2.3 From c61e13ae21cbac7b6642d2d813245c7fc0575834 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 8 Dec 2023 03:08:31 +0000 Subject: Made get_terminal_size and get_cursor_position more robust. --- TUI/module.jai | 125 +++++++++++++++++++++++++++++++++++++++++++++++---------- TUI/unix.jai | 8 ++++ ttt.jai | 91 ++++++++++------------------------------- 3 files changed, 133 insertions(+), 91 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index a12b762..5e19e4f 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -97,6 +97,7 @@ input_buffer : [64] u8; input_string : string; input_override : Key; +row := 5; set_next_key :: (key: Key) { input_override = key; @@ -104,6 +105,22 @@ set_next_key :: (key: Key) { get_key :: (timeout_milliseconds: s32 = -1) -> Key { + // 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; + } + + /* TODO WIP Implement UTF-8 support, i.e., merge continuation bytes if needed: @@ -123,23 +140,73 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { } if OS_was_terminal_resized() return xx Keys.Resize; - + + // FIXME Old version... + // if input_string.count > 0 { + // defer advance(*input_string, 1); + // return input_string[0]; + // } + // FIXME New version... if input_string.count > 0 { - defer advance(*input_string, 1); - return input_string[0]; + utf8_bytes := count_utf8_bytes(input_string[0]); + key: Key; + for 1..utf8_bytes { + key = key << 8; + key |= input_string[utf8_bytes-it]; + } + advance(*input_string, utf8_bytes); + return key; } is_input_available := OS_wait_for_input(timeout_milliseconds); if OS_was_terminal_resized() return xx Keys.Resize; + // FIXME Old version... + // if is_input_available { + // bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. + // if bytes_read > 0 { + // input_string.data = input_buffer.data; + // input_string.count = bytes_read; + // defer advance(*input_string, 1); + // return input_string[0]; + // } + // } + // FIXME New version... if is_input_available { bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. if bytes_read > 0 { input_string.data = input_buffer.data; input_string.count = bytes_read; - defer advance(*input_string, 1); - return input_string[0]; + utf8_bytes := count_utf8_bytes(input_string[0]); + + key: Key; + for 1..utf8_bytes { + key = key << 8; + key |= input_string[utf8_bytes-it]; + } + + /// /// /// /// /// /// /// /// /// + // DEBUG + br, bc := get_cursor_position(); + column := 3; + row += 1; + nr, rc := get_terminal_size(); + + if row >= (rc - 5) then row = 5; + + set_cursor_position(row, column); + for 0..input_string.count-1 { + print("%:% | ", + FormatInt.{base= 2, minimum_digits = 8, value = input_string[it]}, + FormatInt.{base= 16, minimum_digits = 2, value = input_string[it]}, + ); // TODO DEBUG + } + set_cursor_position(br, bc); + /// /// /// /// /// /// /// /// /// + + advance(*input_string, utf8_bytes); + return key; } } return xx Keys.None; @@ -191,7 +258,7 @@ stop :: () { } flush_input :: () { - // TODO + OS_flush_input(); } draw_box :: (x: int, y: int, width: int, height: int) { @@ -256,16 +323,23 @@ get_terminal_size :: () -> rows: int, columns: int { flush_input(); write_string(Commands.QueryWindowSizeInChars); - input := get_str(64); - - // Expected message format: [8;;t + + // Expected response format: \e[8;;t // where is the number of rows and of columns. - assert( - input[0] == #char "\e" && - input[1] == #char "[" && - input[2] == #char "8", - //input[input.count-1] == #char "t", + FORMAT :: "\e[8;;t"; + + // 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 >= 2 && input[input.count-1] != FORMAT[FORMAT.count-1] { + input.count -= 1; + } + + assert(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, ";"); @@ -289,17 +363,26 @@ get_cursor_position :: () -> row: int, column: int { flush_input(); write_string(Commands.QueryCursorPosition); - input := get_str(64); - // Expected message format: \e[;R + // Expected response format: \e[;R // where is the number of rows and of columns. - assert( - input[0] == #char "\e" && - input[1] == #char "[" && - input[input.count-1] == #char "R", - "Query cursor position returned invalid response."); + FORMAT :: "\e[;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[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, ";"); row := parse_int(*parts[0]); diff --git a/TUI/unix.jai b/TUI/unix.jai index fb79f86..73a763a 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -25,6 +25,9 @@ // 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; + // 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 @@ -169,6 +172,11 @@ restore_resize_handler :: () { #scope_export +OS_flush_input :: inline () { + TCIFLUSH :: 0; // TODO Is this always zero in all systems? + tcflush(STDIN_FILENO, TCIFLUSH); +} + OS_prepare_terminal :: () { tcgetattr(STDIN_FILENO, *initial_tio_mode); // TODO Log on error. raw_tio_mode = initial_tio_mode; diff --git a/ttt.jai b/ttt.jai index 110d089..1a25143 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1181,6 +1181,7 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo main :: () { + // -- -- -- Testing TUI -- START #if 0 { print("test 0\n"); TUI.test(); @@ -1258,86 +1259,36 @@ main :: () { TUI.start(); xcolumns, xrows: int; key: TUI.Key = #char "d"; + last_none_char := "X"; while(key != #char "q") { // __mark := get_temporary_storage_mark(); + + if key == { + case xx TUI.Keys.None; { + TUI.set_cursor_position(2, 2); + last_none_char = ifx last_none_char == "X" then "+" else "X"; + write_string(last_none_char); + } + + case xx TUI.Keys.Resize; { + TUI.clear_terminal(); + } + } + size_r, size_c := TUI.get_terminal_size(); + TUI.draw_box(1, 1, size_c, size_r); + x := ifx size_r > 1 then size_r-1 else 1; + y := ifx size_c > 24 then size_c-24 else 1; + TUI.set_cursor_position(x, y); + print("size(CxR): %x%\n", size_c, size_r); - TUI.set_cursor_position(3, 3); - write_string("dam "); - print("%:%", xcolumns, xrows); key = TUI.get_key(3000); - if key == xx TUI.Keys.None { - write_string(">bazinga<"); - sleep_milliseconds(1000); - } - else if key == xx TUI.Keys.Resize { - TUI.clear_terminal(); - xrows, xcolumns = TUI.get_terminal_size(); - TUI.draw_box(1, 1, xcolumns, xrows); - } - else if key == #char "i" { - auto_release_temp(); - TUI.set_cursor_position(7, 3); - write_string("input: "); - TUI.flush_input(); - str := TUI.get_str(3); - print(">%<\n\r", str); - TUI.set_cursor_position(8, 3); - } - else { - print_character(cast(u8)key); - } } TUI.stop(); print("size(CxR): %x%\n", xcolumns, xrows); return; } - - // -- -- -- TODO WIP Testing TUI -- START - // TUI.start(); - // TUI.clear_terminal(); - // rows, columns := TUI.get_terminal_size(); - // TUI.draw_box(1, 1, columns, rows); - // TUI.set_cursor_position(4, 4); - // c_row, c_column := TUI.get_cursor_position(); - // TUI.set_cursor_position(3, 3); - // input := TUI.read_input(); - - // sleep_milliseconds(5000); - // TUI.stop(); - // print("- - -\n"); - // print("window r:c = %:%\n", rows, columns); - // print("cursor r:c = %:%\n", c_row, c_column); - // print("input = %\n", input); - // return; -// -// - // str: string; -// - // write_string("\e(0"); // Enter Line drawing mode - // write_string("\e[104;93m"); // bright yellow on bright blue - // write_string("x"); // in line drawing mode, \x78 -> \u2502 "Vertical Bar" - // write_string("\e[10m"); // restore color - // write_string("\e(B"); // exit line drawing mode - // sleep_milliseconds(1000); - - - // // str = "\e[sWOW\e[u\e[1P"; - // str = "\e[sWOW"; - // // write(STDIN_FILENO, str.data, xx str.count); - // write_string(str); - // sleep_milliseconds(1000); - // - // str = "\e[?25l"; - // // write(STDIN_FILENO, str.data, xx str.count); - // write_string(str); - // sleep_milliseconds(1000); - // - // str = "\e[?25h"; - // // write(STDIN_FILENO, str.data, xx str.count); - // write_string(str); - // return; - // -- -- -- TODO WIP Testing TUI -- STOP + // -- -- -- Testing TUI -- STOP // TODO Implement signal handling and see modules/Debug.jai for examples. -- cgit v1.2.3 From 58716ceff7f82ee8b01ea43734cc755cacff25a6 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 11 Dec 2023 01:28:50 +0000 Subject: Add set_terminal_title procedure. --- TUI/module.jai | 13 +++++++++++++ ttt.jai | 1 + 2 files changed, 14 insertions(+) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index c8a5512..044fd65 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -43,6 +43,10 @@ Commands :: struct { ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; + Bell :: "\x07"; + + SetWindowTitle :: "\e]0;%\x07"; + RefreshWindow :: "\e[7t"; // TODO Not yet tested. SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE @@ -99,6 +103,10 @@ input_override : Key; row := 5; +assert_is_initialized :: inline () { + assert(initialized, "TUI is not ready."); +} + set_next_key :: (key: Key) { input_override = key; } @@ -392,6 +400,11 @@ get_cursor_position :: () -> row: int, column: int { return row, column; } +set_terminal_title :: (title: string) { + assert_is_initialized(); + print(Commands.SetWindowTitle, title); +} + #if OS == .WINDOWS { // Prototyping zone... keep clear! diff --git a/ttt.jai b/ttt.jai index 1a25143..49e88eb 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1257,6 +1257,7 @@ main :: () { #if 1 { print("test 4\n", to_standard_error = true); TUI.start(); + TUI.set_terminal_title("bazinga"); xcolumns, xrows: int; key: TUI.Key = #char "d"; last_none_char := "X"; -- cgit v1.2.3 From e3f4ad1b4db98f612f097ba76116ab5d85bed912 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 12 Dec 2023 03:03:45 +0000 Subject: Improved tests and tried to implement tcsetattr by relying only on ioctl. --- TUI/module.jai | 46 ++++++++++++----------- TUI/unix.jai | 88 +++++++++++++++++++++++++++++++++++--------- ttt.jai | 114 +++++++++++++++++++++++++++++++++++++-------------------- 3 files changed, 170 insertions(+), 78 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 044fd65..bfed510 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -108,11 +108,13 @@ assert_is_initialized :: inline () { } set_next_key :: (key: Key) { + assert_is_initialized(); input_override = key; } get_key :: (timeout_milliseconds: s32 = -1) -> Key { - + assert_is_initialized(); + // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte is_utf8_continuation_byte :: inline (byte: u8) -> bool { return (byte & 0xC0) == 0x80; @@ -196,21 +198,21 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { /// /// /// /// /// /// /// /// /// // DEBUG - br, bc := get_cursor_position(); - column := 3; - row += 1; - nr, rc := get_terminal_size(); - - if row >= (rc - 5) then row = 5; - - set_cursor_position(row, column); - for 0..input_string.count-1 { - print("%:% | ", - FormatInt.{base= 2, minimum_digits = 8, value = input_string[it]}, - FormatInt.{base= 16, minimum_digits = 2, value = input_string[it]}, - ); // TODO DEBUG - } - set_cursor_position(br, bc); + // br, bc := get_cursor_position(); + // column := 3; + // row += 1; + // nr, rc := get_terminal_size(); + // + // if row >= (rc - 5) then row = 5; +// + // set_cursor_position(row, column); + // for 0..input_string.count-1 { + // print("%:% | ", + // FormatInt.{base= 2, minimum_digits = 8, value = input_string[it]}, + // FormatInt.{base= 16, minimum_digits = 2, value = input_string[it]}, + // ); // TODO DEBUG + // } + // set_cursor_position(br, bc); /// /// /// /// /// /// /// /// /// advance(*input_string, utf8_bytes); @@ -221,6 +223,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { } get_str :: (count_limit: int = -1, allocator: Allocator = temp) -> string { + assert_is_initialized(); assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); if count_limit < 0 { @@ -270,6 +273,7 @@ flush_input :: () { } 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. @@ -315,13 +319,13 @@ draw_box :: (x: int, y: int, width: int, height: int) { // TODO Maybe rename to "clear()" clear_terminal :: inline () { - assert(initialized, "TUI is not ready."); + assert_is_initialized(); write_string(Commands.ClearScreen); } // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { - assert(initialized, "TUI is not ready."); + assert_is_initialized(); rows, columns: int = ---; #if OS == .WINDOWS { rows, columns = OS_get_terminal_size(); @@ -359,15 +363,15 @@ get_terminal_size :: () -> rows: int, columns: int { } set_cursor_position :: (row: int, column: int) { - assert(initialized, "TUI is not ready."); + assert_is_initialized(); auto_release_temp(); tmp_string := tprint(Commands.SetCursorPosition, row, column); write_string(tmp_string); } get_cursor_position :: () -> row: int, column: int { - assert(initialized, "TUI is not ready."); // TODO Should I use this inside each and every procedure? - + assert_is_initialized(); + auto_release_temp(); flush_input(); diff --git a/TUI/unix.jai b/TUI/unix.jai index 2c34122..03a371b 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -6,19 +6,9 @@ // Required to do unlocking input. libc :: #system_library "libc"; - - - // int poll(struct pollfd *fds, nfds_t nfds, int timeout); - pollfd :: struct { - fd : s32; // File descriptor. - events : s16; // Requested events. - revents : s16; // Returned events. - }; - poll :: (fds: *pollfd, nfds: u32, timeout: s32) -> s32 #foreign libc; - // TODO Remote this. - cfmakeraw :: (termios: *Terminal_IO_Mode) -> void #foreign libc; + // 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) @@ -32,19 +22,81 @@ 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 #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 :: (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. @@ -187,7 +239,7 @@ OS_flush_input :: inline () { } OS_prepare_terminal :: () { - tcgetattr(STDIN_FILENO, *initial_tio_mode); // TODO Log on error. + 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); diff --git a/ttt.jai b/ttt.jai index 49e88eb..b8681ff 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1182,36 +1182,40 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo main :: () { // -- -- -- Testing TUI -- START - #if 0 { - print("test 0\n"); - TUI.test(); - return; - } + + // TODO Test input #if 0 { print("test 1\n"); TUI.start(); buffer: [8] u8; - brss, error, msg := TUI.OS_read_input(buffer.data, buffer.count); + bytes_read, error, error_msg := TUI.OS_read_input(buffer.data, buffer.count); TUI.stop(); - if error == true print("error:%", msg); - print("br:%", brss); - print("input:%", cast(string)buffer); + if error == true print("error:%\n", error_msg); + print("read % bytes:", bytes_read); + for 0..bytes_read-1 { + char := "-NA-"; + if buffer[it] >= 33 && buffer[it] <= 126 { + char.data = *buffer[it]; + char.count = 1; + } + print(" 0x% (%)", FormatInt.{minimum_digits=2, base=10, value=buffer[it]}, char); + } + print("\n"); TUI.stop(); - return; } #if 0 { print("test 2\n"); - key: TUI.Key = #char "d"; - while(key != #char "q") { + _key: TUI.Key = #char "d"; + while(_key != #char "q") { // key = TUI.get_key(10); buffer: [8] u8; buffer[0] = 0; - brss := TUI.OS_read_input(buffer.data, 1); - if brss > 0 { - key = buffer[0]; - print(">%<", xx key); + bytes_read := TUI.OS_read_input(buffer.data, 1); + if bytes_read > 0 { + _key = buffer[0]; + print(">%<", xx _key); } else { print(":"); @@ -1219,43 +1223,72 @@ main :: () { sleep_milliseconds(10); } TUI.stop(); - return; } - #if 0 { - print("testing select\n"); + if 1 { + print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); - TUI.test(); - TUI.test(); - TUI.test(); + ROW :: 3; + COLUMN :: 3; + TUI.set_cursor_position(ROW, COLUMN); + row, column := TUI.get_cursor_position(); TUI.stop(); - return; + assert(row == ROW && column == COLUMN, "Failed set/get cursor position.\n"); + print("> success\n", to_standard_error = true); } - - #if 0 { - print("test 3\n"); + + if 1 { + print("TEST : test key input --\n", to_standard_error = true); + auto_release_temp(); TUI.start(); - key: TUI.Key = #char "d"; + 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."); + TUI.set_cursor_position(2, 1); + key: TUI.Key; while(key != #char "q") { __mark := get_temporary_storage_mark(); - // sleep_milliseconds(1000); - key = TUI.get_key(3000); - - if key != xx TUI.Keys.None print_character(key); + key = TUI.get_key(1000); + if key != xx TUI.Keys.None print_character(cast,force(u8)key); else write_string("-"); set_temporary_storage_mark(__mark); } - print("Waiting 1s.."); - sleep_milliseconds(1000); - print("!\n\r"); - print("Stoping.."); TUI.stop(); - print("!\n\r"); - return; + print("> success\n", to_standard_error = true); + } + + if 1 { + print("TEST : draw box --\n", to_standard_error = true); + auto_release_temp(); + TUI.start(); + 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(); + TUI.stop(); + assert(key == #char "y", "Failed to draw box.\n"); + print("> success\n", to_standard_error = true); + } + + if 1 { + print("TEST : get terminal size --\n", to_standard_error = true); + auto_release_temp(); + TUI.start(); + TUI.clear_terminal(); + rows, columns := TUI.get_terminal_size(); + TUI.set_cursor_position(1, 1); + print("Is terminal size % columns and % rows? (y/n)", columns, rows); + key := TUI.get_key(); + TUI.stop(); + assert(key == #char "y", "Failed to get terminal size.\n"); + print("> success\n", to_standard_error = true); } - #if 1 { - print("test 4\n", to_standard_error = true); + // TODO Resizing terminal breaks the tests... try to fix it. + + #if 0 { + print("test 5\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); xcolumns, xrows: int; @@ -1286,6 +1319,9 @@ main :: () { } TUI.stop(); print("size(CxR): %x%\n", xcolumns, xrows); + } + + #if true { return; } -- cgit v1.2.3 From f75b85e0dac3ee213b8d77cec8f1669881f99d18 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 13 Dec 2023 00:48:05 +0000 Subject: Improved TUI tests. --- ttt.jai | 49 +++++-------------------------------------------- 1 file changed, 5 insertions(+), 44 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index b8681ff..f0b554f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1185,46 +1185,6 @@ main :: () { // TODO Test input - #if 0 { - print("test 1\n"); - TUI.start(); - buffer: [8] u8; - bytes_read, error, error_msg := TUI.OS_read_input(buffer.data, buffer.count); - TUI.stop(); - if error == true print("error:%\n", error_msg); - print("read % bytes:", bytes_read); - for 0..bytes_read-1 { - char := "-NA-"; - if buffer[it] >= 33 && buffer[it] <= 126 { - char.data = *buffer[it]; - char.count = 1; - } - print(" 0x% (%)", FormatInt.{minimum_digits=2, base=10, value=buffer[it]}, char); - } - print("\n"); - TUI.stop(); - } - - #if 0 { - print("test 2\n"); - _key: TUI.Key = #char "d"; - while(_key != #char "q") { - // key = TUI.get_key(10); - buffer: [8] u8; - buffer[0] = 0; - bytes_read := TUI.OS_read_input(buffer.data, 1); - if bytes_read > 0 { - _key = buffer[0]; - print(">%<", xx _key); - } - else { - print(":"); - } - sleep_milliseconds(10); - } - TUI.stop(); - } - if 1 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); @@ -1249,7 +1209,7 @@ main :: () { while(key != #char "q") { __mark := get_temporary_storage_mark(); key = TUI.get_key(1000); - if key != xx TUI.Keys.None print_character(cast,force(u8)key); + if key >= 33 && key <= 128 then print_character(cast,force(u8)key); else write_string("-"); set_temporary_storage_mark(__mark); } @@ -1279,13 +1239,14 @@ main :: () { rows, columns := TUI.get_terminal_size(); TUI.set_cursor_position(1, 1); print("Is terminal size % columns and % rows? (y/n)", columns, rows); - key := TUI.get_key(); + key: TUI.Key = xx TUI.Keys.None; + while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { + key = TUI.get_key(); + } TUI.stop(); assert(key == #char "y", "Failed to get terminal size.\n"); print("> success\n", to_standard_error = true); } - - // TODO Resizing terminal breaks the tests... try to fix it. #if 0 { print("test 5\n", to_standard_error = true); -- cgit v1.2.3 From 47890a446f1b9827fe2dc8ac370ded9d91ed2247 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 14 Dec 2023 01:05:42 +0000 Subject: Testing Key structures and procedures. --- ttt.jai | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index b8681ff..9ef7543 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1185,6 +1185,66 @@ main :: () { // TODO Test input + Key :: union { + text: [4] u8; + code: u64; + } + + // Instead of checking if it's an array or string... check the type is array of u8. + to_key :: inline (value: $T) -> Key #modify { return T == u8 || T == string || (T == []u8); } { + k: Key; + #if T == u8 { + k.text[0] = value; + } + #if T == string || (T == []u8) { + assert(value.count <= 4); + for 0..value.count-1 k.text[it] = value[it]; + } + return k; + } + + operator == :: inline (a: Key, b: Key) -> bool { + return a.code == b.code; + } + + // Instead of checking if it's an array or string... check the type is array of u8. + // We'll need to use "Type_Info" and "Type_Info_Array" + // and cast T using `type_info := cast(*Type_Info)T;` + operator == :: inline (a: Key, b: $T) -> bool + #modify { + if T == u8 { + return true; + } + ti := cast(*Type_Info)T; + if ti.type == .ARRAY { + ti_array := cast(*Type_Info_Array)T; + // tia_type := ti_array.element_type.type; + return ti_array.element_type.type == .INTEGER && ti_array.element_type.runtime_size == 1; + } + return false; + // return T == u8 || T == string || (T == []u8); } { + } { + // operator == :: inline (a: Key, b: $T) -> bool #modify { return (T == [4]u8); } { + k := to_key(b); + return a == k; + } + + // k := K. {xx "01"}; + // k := to_key(xx "\0\0\01"); + // k := to_key(xx "€"); + // k := to_key(xx "A"); + k := to_key("A"); + // a: u8 = #char "A"; + a: [4]u8; + a[0] = #char "A"; + print("Is equal to 'A': >%<\n", k == a); + // b: u8 = 3; + // print("Is equal to 'A': >%<\n", k == #char); + print("Text :>%<\n", cast(string)k.text, to_standard_error = true); + print("Key :>%<\n", FormatInt.{value=k.code, base=16, minimum_digits=8}, to_standard_error = true); + return; + + #if 0 { print("test 1\n"); TUI.start(); @@ -1284,8 +1344,6 @@ main :: () { assert(key == #char "y", "Failed to get terminal size.\n"); print("> success\n", to_standard_error = true); } - - // TODO Resizing terminal breaks the tests... try to fix it. #if 0 { print("test 5\n", to_standard_error = true); -- cgit v1.2.3 From 8d06729eb3353705f5d35457c5f921407f8c0f4a Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 15 Dec 2023 02:02:33 +0000 Subject: WIP: Trying to define Key. --- TUI/module.jai | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++----- ttt.jai | 60 --------------------------------------------------------- 2 files changed, 56 insertions(+), 65 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index bfed510..48fca16 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -86,14 +86,65 @@ Commands :: struct { // TODO Maybe make the OS_* procedures as inline?! -// Let's return keys with 32bits so we can merge up to 4bytes and support UTF-8 encoding. -Key :: u32; +// 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). + +Key :: union { + text: [8] u8; + code: u64; +} + +to_key :: inline (value: $T) -> Key #modify { return T == u8 || T == ([]u8) || T == string; } { + k: Key; + #if T == u8 { + k.text[0] = value; + } + #if T == ([]u8) || T == string { + size := ifx value.count > Key.text.count then Key.text.count else value.count; + // assert(size <= Key.text.count); This is now under the user responsability. + memcpy(k.text.data, value.data, size); + } + return k; +} + +operator == :: inline (a: Key, b: Key) -> bool { + return a.code == b.code; +} + +operator == :: inline (a: Key, b: $T) -> bool { + k := to_key(b); + return a.code == k.code; +} + +#run { + print("\n:::%\n", Key.text.count); + // ti := type_info(Key); + print("\n---\n%\n---\n", type_info(Key).*); + a: Key; + b: u8; + print(">%\n", a == to_key(b)); + + c: string = ""; + print(">%\n", a == to_key(c)); + + d: []u8 = xx ""; + print(">%\n", a == to_key(d)); + + print(">%\n", a == b); + print(">%\n", a == c); + print(">%\n", a == d); +} // Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. -Keys :: enum Key { - None :: 0xFF000000; - Resize :: 0xFF000001; +Keys :: struct { + None :: #run to_key("\0"); + Resize :: #run to_key(xx 1); } +// Keys :: enum Key { + // None :: 0xFF000000; + // Resize :: 0xFF000001; +// } initialized := false; diff --git a/ttt.jai b/ttt.jai index 25bf782..f0b554f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1185,66 +1185,6 @@ main :: () { // TODO Test input - Key :: union { - text: [4] u8; - code: u64; - } - - // Instead of checking if it's an array or string... check the type is array of u8. - to_key :: inline (value: $T) -> Key #modify { return T == u8 || T == string || (T == []u8); } { - k: Key; - #if T == u8 { - k.text[0] = value; - } - #if T == string || (T == []u8) { - assert(value.count <= 4); - for 0..value.count-1 k.text[it] = value[it]; - } - return k; - } - - operator == :: inline (a: Key, b: Key) -> bool { - return a.code == b.code; - } - - // Instead of checking if it's an array or string... check the type is array of u8. - // We'll need to use "Type_Info" and "Type_Info_Array" - // and cast T using `type_info := cast(*Type_Info)T;` - operator == :: inline (a: Key, b: $T) -> bool - #modify { - if T == u8 { - return true; - } - ti := cast(*Type_Info)T; - if ti.type == .ARRAY { - ti_array := cast(*Type_Info_Array)T; - // tia_type := ti_array.element_type.type; - return ti_array.element_type.type == .INTEGER && ti_array.element_type.runtime_size == 1; - } - return false; - // return T == u8 || T == string || (T == []u8); } { - } { - // operator == :: inline (a: Key, b: $T) -> bool #modify { return (T == [4]u8); } { - k := to_key(b); - return a == k; - } - - // k := K. {xx "01"}; - // k := to_key(xx "\0\0\01"); - // k := to_key(xx "€"); - // k := to_key(xx "A"); - k := to_key("A"); - // a: u8 = #char "A"; - a: [4]u8; - a[0] = #char "A"; - print("Is equal to 'A': >%<\n", k == a); - // b: u8 = 3; - // print("Is equal to 'A': >%<\n", k == #char); - print("Text :>%<\n", cast(string)k.text, to_standard_error = true); - print("Key :>%<\n", FormatInt.{value=k.code, base=16, minimum_digits=8}, to_standard_error = true); - return; - - if 1 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); -- cgit v1.2.3 From 40b9a45a41f894cd8569104155c7456d1ef6f6bc Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 17 Dec 2023 02:24:39 +0000 Subject: It seems we arrived a good compromise for the Key structure. --- TUI/module.jai | 153 ++++++++++++++++++++++++++++++++++++++++++++------------- ttt.jai | 3 +- 2 files changed, 120 insertions(+), 36 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 48fca16..0d0f468 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -90,38 +90,115 @@ Commands :: struct { // 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). -Key :: union { - text: [8] u8; - code: u64; -} -to_key :: inline (value: $T) -> Key #modify { return T == u8 || T == ([]u8) || T == string; } { - k: Key; - #if T == u8 { - k.text[0] = value; +/* + In this block, we're testing having the Key as a union of both [8]u8 and u64. + It kinda looks OK, but it may drive the users to extract info from the code while + it should be used only for comparison. +*/ +test_union :: () +{ + + K :: union { + // #as text: [8] u8; + text: [8] u8; + code: u64; } - #if T == ([]u8) || T == string { - size := ifx value.count > Key.text.count then Key.text.count else value.count; - // assert(size <= Key.text.count); This is now under the user responsability. - memcpy(k.text.data, value.data, size); + + to_key :: inline (value: $T) -> K #modify { return T == u8 || T == ([]u8) || T == string; } { + k: K; + #if T == u8 { + k.text[0] = value; + } + #if T == ([]u8) || T == string { + size := ifx value.count > K.text.count then K.text.count else value.count; + // assert(size <= K.text.count); This is now under the user responsability. + memcpy(k.text.data, value.data, size); + } + return k; } - return k; -} + + operator == :: inline (a: K, b: K) -> bool { + return a.code == b.code; + } + + operator == :: inline (a: K, b: $T) -> bool { + k := to_key(b); + return a.code == k.code; + } + + + // ti := type_info(K); + print("\n---\n%\n---\n", type_info(K).*); + a: K; + b: u8; + print(">%\n", a == to_key(b)); -operator == :: inline (a: Key, b: Key) -> bool { - return a.code == b.code; -} + c: string = ""; + print(">%\n", a == to_key(c)); -operator == :: inline (a: Key, b: $T) -> bool { - k := to_key(b); - return a.code == k.code; + d: []u8 = xx ""; + print(">%\n", a == to_key(d)); + + print(">%\n", a == b); + print(">%\n", a == c); + print(">%\n", a == d); + + zed := K.{ xx "Bazinga!" }; + zed = to_key("abc"); + + print("\n- - -\n%\n- - -\n", cast(string)zed.text); + + print("$ $ $\n%\n$ $ $\n", zed == "abc"); + } +#run test_union(); + +/* + In this block, we're testing having the Key as: [8] u8 + ...WIP... +*/ +/* +test_array :: () +{ + K :: #type,distinct [8] u8; + K_count :: 8;// #run type_info(K).array_count; + // #run print("###\n%\n###\n", type_info(K).*); + // #run print("###\n%\n###\n", type_info(K).array_count); + // #run print(":%", type_info(K).*); + // #run print(":%", ; + // #run print("\n###\n"); + // #run print("%", cast(*Type_Info_Array)type_info(K).array_count); + // #run print("%", type_info(K).count); + // #run print("\n###\n"); + + to_key :: inline (value: $T) -> K #modify { return T == u8 || T == ([]u8) || T == string; } { + k: K; + #if T == u8 { + k[0] = value; + } + #if T == ([]u8) || T == string { + size := ifx value.count > K_count then K_count else value.count; + // assert(size <= K.text.count); This is now under the user responsability. + memcpy(k.data, value.data, size); + } + return k; + } + + operator == :: inline (a: K, b: K) -> bool #dump { + // return memcmp(a.data, b.data, 8) == 0; + return (cast,force(*u64)*a).* == (cast,force(*u64)*b).*; + } + + operator == :: inline (a: K, b: $T) -> bool { + k := to_key(b); + return a == k; + } -#run { - print("\n:::%\n", Key.text.count); - // ti := type_info(Key); - print("\n---\n%\n---\n", type_info(Key).*); - a: Key; + print("\n:::%\n", K_count); + // ti := type_info(K); + print("\n---\n%\n---\n", type_info(K).*); + a: K; b: u8; print(">%\n", a == to_key(b)); @@ -131,20 +208,26 @@ operator == :: inline (a: Key, b: $T) -> bool { d: []u8 = xx ""; print(">%\n", a == to_key(d)); - print(">%\n", a == b); - print(">%\n", a == c); - print(">%\n", a == d); + // print(">%\n", a == b); + // print(">%\n", a == c); + // print(">%\n", a == d); } +#run test_array(); +*/ + +// Rest of the module --- + +Key :: u32; // Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. -Keys :: struct { - None :: #run to_key("\0"); - Resize :: #run to_key(xx 1); -} -// Keys :: enum Key { - // None :: 0xFF000000; - // Resize :: 0xFF000001; +// Keys :: struct { + // None :: #run to_key("\0"); + // Resize :: #run to_key(xx 1); // } +Keys :: enum Key { + None :: 0xFF000000; + Resize :: 0xFF000001; +} initialized := false; diff --git a/ttt.jai b/ttt.jai index f0b554f..1cae6a2 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1179,7 +1179,8 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo return read_input_char(row, style, message) == #char "\n"; } -main :: () { +main :: () {} +_main :: () { // -- -- -- Testing TUI -- START -- cgit v1.2.3 From 6dcfeb56e6c65724a76afc8beedc277c8f4f7171 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 20 Dec 2023 02:24:28 +0000 Subject: Attempt at implementing Key and get_key(). --- TUI/module.jai | 232 ++++++++++++++++++++++++--------------------------------- ttt.jai | 4 +- 2 files changed, 99 insertions(+), 137 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 0d0f468..78b7605 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -11,6 +11,14 @@ #import "Thread"; Drawings :: struct { + // TODO Maybe just use unicode instead of jumping back and forth between drawing mode?! + // test_drawing_without_mode :: () { + // top := "┌───┐"; + // btm := "└───┘"; + // print(">%<\n", top); + // print(">%<\n", btm); + // } + // #run test_drawing_without_mode(); CornerBR :: "\x6A"; CornerTR :: "\x6B"; CornerTL :: "\x6C"; @@ -96,111 +104,56 @@ Commands :: struct { It kinda looks OK, but it may drive the users to extract info from the code while it should be used only for comparison. */ -test_union :: () -{ - - K :: union { - // #as text: [8] u8; - text: [8] u8; - code: u64; - } - - to_key :: inline (value: $T) -> K #modify { return T == u8 || T == ([]u8) || T == string; } { - k: K; - #if T == u8 { - k.text[0] = value; - } - #if T == ([]u8) || T == string { - size := ifx value.count > K.text.count then K.text.count else value.count; - // assert(size <= K.text.count); This is now under the user responsability. - memcpy(k.text.data, value.data, size); - } - return k; - } - - operator == :: inline (a: K, b: K) -> bool { - return a.code == b.code; - } - - operator == :: inline (a: K, b: $T) -> bool { - k := to_key(b); - return a.code == k.code; - } - - - // ti := type_info(K); - print("\n---\n%\n---\n", type_info(K).*); - a: K; - b: u8; - print(">%\n", a == to_key(b)); - c: string = ""; - print(">%\n", a == to_key(c)); - d: []u8 = xx ""; - print(">%\n", a == to_key(d)); - print(">%\n", a == b); - print(">%\n", a == c); - print(">%\n", a == d); - - zed := K.{ xx "Bazinga!" }; - zed = to_key("abc"); - - print("\n- - -\n%\n- - -\n", cast(string)zed.text); +Key :: u64; - print("$ $ $\n%\n$ $ $\n", zed == "abc"); - +KEY_SIZE :: #run type_info(Key).runtime_size; + +Keys :: enum Key { + // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. + None :: 0xF0000000_0000000F; + Resize :: 0xF0000000_0000001F; } -#run test_union(); -/* - In this block, we're testing having the Key as: [8] u8 - ...WIP... +/* TODO + 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|) */ -/* -test_array :: () -{ - K :: #type,distinct [8] u8; - K_count :: 8;// #run type_info(K).array_count; - // #run print("###\n%\n###\n", type_info(K).*); - // #run print("###\n%\n###\n", type_info(K).array_count); - // #run print(":%", type_info(K).*); - // #run print(":%", ; - // #run print("\n###\n"); - // #run print("%", cast(*Type_Info_Array)type_info(K).array_count); - // #run print("%", type_info(K).count); - // #run print("\n###\n"); - - to_key :: inline (value: $T) -> K #modify { return T == u8 || T == ([]u8) || T == string; } { - k: K; - #if T == u8 { - k[0] = value; - } - #if T == ([]u8) || T == string { - size := ifx value.count > K_count then K_count else value.count; - // assert(size <= K.text.count); This is now under the user responsability. - memcpy(k.data, value.data, size); - } - return k; - } - operator == :: inline (a: K, b: K) -> bool #dump { - // return memcmp(a.data, b.data, 8) == 0; - return (cast,force(*u64)*a).* == (cast,force(*u64)*b).*; +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; +} - operator == :: inline (a: K, b: $T) -> bool { - k := to_key(b); - return a == k; +to_string :: inline (key: Key) -> string #dump { + 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; +} - print("\n:::%\n", K_count); +test_union :: () +{ // ti := type_info(K); - print("\n---\n%\n---\n", type_info(K).*); - a: K; + print("\n---\n%\n---\n", type_info(Key).*); + print("\n---\n%\n---\n", type_info(Key).runtime_size); + a: Key; b: u8; - print(">%\n", a == to_key(b)); + print(">%\n", a == b); c: string = ""; print(">%\n", a == to_key(c)); @@ -208,26 +161,30 @@ test_array :: () d: []u8 = xx ""; print(">%\n", a == to_key(d)); - // print(">%\n", a == b); - // print(">%\n", a == c); - // print(">%\n", a == d); -} -#run test_array(); -*/ - -// Rest of the module --- + top := "┌───┐"; + btm := "└───┘"; + print(">%<\n", top); + print(">%<\n", btm); -Key :: u32; + str := "abcd"; + tok := to_key(str); + tos := to_string(tok); + + print("1:%:\n", str); + print("2:"); + for 0..7 { + val := tok & 0xFF; + tok >>=8 ; + print("% ", FormatInt.{value=val, base=16, minimum_digits=2}); + } + print(":\n"); + print("3:%:\n", tos); + +} +#run test_union(); // Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. -// Keys :: struct { - // None :: #run to_key("\0"); - // Resize :: #run to_key(xx 1); -// } -Keys :: enum Key { - None :: 0xFF000000; - Resize :: 0xFF000001; -} + initialized := false; @@ -235,7 +192,10 @@ input_buffer : [64] u8; input_string : string; input_override : Key; -row := 5; +#run { + // Some tests. + assert(input_buffer.count >= 6, "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."); @@ -264,20 +224,6 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return 1; } - - /* - TODO WIP Implement UTF-8 support, i.e., merge continuation bytes if needed: - - key = arg[0] - while is_last_utf8_byte(buffer[0]) == false { - key = key << 8 - advance(buffer) - key |= buffer[0] - TODO Sometimes we may not have all the necessary bytes in the buffer... and we may need to fetch some more into it. How to do it? - } - advance(arg) - */ - if input_override != xx Keys.None { defer input_override = xx Keys.None; return input_override; @@ -293,11 +239,26 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // FIXME New version... if input_string.count > 0 { utf8_bytes := count_utf8_bytes(input_string[0]); - key: Key; - for 1..utf8_bytes { - key = key << 8; - key |= input_string[utf8_bytes-it]; + + // TODO We're assuming the input_buffer contains the entirety of what we need to read for the UTF8 symbols... + if utf8_bytes > input_string.count { + + // TODO Test this and make sure it's working... + print(""); + + // Copy buffered bytes to the start, and read the remaining ones from input. + for 0..utf8_bytes-1 { + input_buffer[it] = input_string[it]; + } + OS_read_input(input_buffer.data + utf8_bytes, utf8_bytes-input_string.count); // TODO Does not check for read errors. + input_string.data = input_buffer.data; + input_string.count = utf8_bytes; } + + to_parse := input_string; + to_parse.count = utf8_bytes; + key := to_key(to_parse); + advance(*input_string, utf8_bytes); return key; } @@ -323,12 +284,15 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { input_string.data = input_buffer.data; input_string.count = bytes_read; utf8_bytes := count_utf8_bytes(input_string[0]); + + assert(utf8_bytes <= input_string.count, "The input buffer is too small."); // TODO Improve error message. - key: Key; - for 1..utf8_bytes { - key = key << 8; - key |= input_string[utf8_bytes-it]; - } + to_parse := input_string; + to_parse.count = utf8_bytes; + key := to_key(to_parse); + + advance(*input_string, utf8_bytes); + return key; /// /// /// /// /// /// /// /// /// // DEBUG @@ -338,7 +302,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // nr, rc := get_terminal_size(); // // if row >= (rc - 5) then row = 5; -// + // // set_cursor_position(row, column); // for 0..input_string.count-1 { // print("%:% | ", @@ -349,8 +313,6 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // set_cursor_position(br, bc); /// /// /// /// /// /// /// /// /// - advance(*input_string, utf8_bytes); - return key; } } return xx Keys.None; diff --git a/ttt.jai b/ttt.jai index 1cae6a2..aeb5b37 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1179,8 +1179,8 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo return read_input_char(row, style, message) == #char "\n"; } -main :: () {} -_main :: () { +// main :: () {} +main :: () { // -- -- -- Testing TUI -- START -- cgit v1.2.3 From 3a949ea9f18d06bbefc73da8ca0c1051252ca96d Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 20 Dec 2023 10:00:44 +0000 Subject: Trying to fix get_key to behave well when it's buffer is full. --- TUI/module.jai | 29 ++++++++++++++++++----------- ttt.jai | 16 ++++++++++++---- 2 files changed, 30 insertions(+), 15 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 78b7605..0e2ff22 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -111,10 +111,10 @@ Key :: u64; KEY_SIZE :: #run type_info(Key).runtime_size; -Keys :: enum Key { +Keys :: struct { // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. - None :: 0xF0000000_0000000F; - Resize :: 0xF0000000_0000001F; + None : Key : 0xF0000000_0000000F; + Resize : Key : 0xF0000000_0000001F; } /* TODO @@ -135,7 +135,7 @@ to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } return k; } -to_string :: inline (key: Key) -> string #dump { +to_string :: inline (key: Key) -> string { str := talloc_string(KEY_SIZE); str.count = 0; while key != 0 #no_abc { @@ -154,6 +154,7 @@ test_union :: () a: Key; b: u8; print(">%\n", a == b); + print(">%\n", a != Keys.None); c: string = ""; print(">%\n", a == to_key(c)); @@ -188,7 +189,8 @@ test_union :: () initialized := false; -input_buffer : [64] u8; +// 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; @@ -237,20 +239,25 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // return input_string[0]; // } // FIXME New version... - if input_string.count > 0 { + if input_string.count > 0 #no_abc { // TODO Some trickery requires no_abc... which is not that nice in this case... utf8_bytes := count_utf8_bytes(input_string[0]); - // TODO We're assuming the input_buffer contains the entirety of what we need to read for the UTF8 symbols... + // TODO We're assuming the terminal buffer contains the entirety of what we need to read for the UTF8 symbols. if utf8_bytes > input_string.count { - // TODO Test this and make sure it's working... - print(""); + diff := utf8_bytes - input_string.count; + + // TODO Test this and make sure it's working...drop the following lines using Ctrl+V to fill the terminal buffer at once: + // d€€€a + // 1234567890123456789012345678901234567890 + print(""); // Copy buffered bytes to the start, and read the remaining ones from input. - for 0..utf8_bytes-1 { + for 0..input_string.count-1 { input_buffer[it] = input_string[it]; } - OS_read_input(input_buffer.data + utf8_bytes, utf8_bytes-input_string.count); // TODO Does not check for read errors. + aaa := OS_read_input(input_buffer.data + input_string.count, utf8_bytes-input_string.count); // TODO Does not check for read errors. + assert(aaa == diff, "READ MORE THAN EXPECTED"); input_string.data = input_buffer.data; input_string.count = utf8_bytes; } diff --git a/ttt.jai b/ttt.jai index aeb5b37..80d89bf 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1179,7 +1179,6 @@ read_enter_confirmation :: inline (row: int, style: int, message: string) -> boo return read_input_char(row, style, message) == #char "\n"; } -// main :: () {} main :: () { // -- -- -- Testing TUI -- START @@ -1249,25 +1248,34 @@ main :: () { print("> success\n", to_standard_error = true); } - #if 0 { + #if 1 { print("test 5\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); xcolumns, xrows: int; key: TUI.Key = #char "d"; last_none_char := "X"; + drop_down := 0; while(key != #char "q") { // __mark := get_temporary_storage_mark(); if key == { - case xx TUI.Keys.None; { + case TUI.Keys.None; { TUI.set_cursor_position(2, 2); last_none_char = ifx last_none_char == "X" then "+" else "X"; write_string(last_none_char); } - case xx TUI.Keys.Resize; { + case TUI.Keys.Resize; #through; + case #char "c"; { TUI.clear_terminal(); + drop_down = 0; + } + + case; { + TUI.set_cursor_position(3+drop_down, 2); + write_string(TUI.to_string(key)); + drop_down += 1; } } size_r, size_c := TUI.get_terminal_size(); -- cgit v1.2.3 From bff4f889c49587fffd39ceb04c88db42bba8d8f1 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 21 Dec 2023 19:57:36 +0000 Subject: Started to use TUI in ttt. --- TUI/module.jai | 20 +++++++++++++--- ttt.jai | 72 ++++++++++++++++++++++++++++++++++------------------------ 2 files changed, 59 insertions(+), 33 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 0e2ff22..bb5b800 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -115,6 +115,14 @@ Keys :: struct { // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. None : Key : 0xF0000000_0000000F; Resize : Key : 0xF0000000_0000001F; + + 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"); + + PgUp : Key : #run to_key("\e[5~"); + PgDown : Key : #run to_key("\e[6~"); } /* TODO @@ -293,12 +301,18 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { utf8_bytes := count_utf8_bytes(input_string[0]); assert(utf8_bytes <= input_string.count, "The input buffer is too small."); // TODO Improve error message. - + + // TODO This is only being done after the OS_wait_for_input... for now! to_parse := input_string; to_parse.count = utf8_bytes; - key := to_key(to_parse); - advance(*input_string, utf8_bytes); + // Must be a terminal escape sequence. + if utf8_bytes == 1 && input_string[0] == #char "\e" { + to_parse.count = ifx input_string.count > 6 then 6 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; /// /// /// /// /// /// /// /// /// diff --git a/ttt.jai b/ttt.jai index 80d89bf..11bf9f4 100644 --- a/ttt.jai +++ b/ttt.jai @@ -902,7 +902,7 @@ initialize_tui :: () { } // TODO DAM - TIO.initialize(); + TUI.start(); // stdscr = initscr(); // Start curses mode. TODO DAM // cbreak(); // Line buffering disabled; pass on everty thing to me. TODO DAM // keypad(stdscr, true); // I need those nifty F1..F12. TODO DAM @@ -956,11 +956,10 @@ draw_tui :: (db: *Database, layout: *Layout) { // Reset theme and clear screen. //attrset(A_NORMAL); TODO DAM - TIO.tui_clear_screen(); + TUI.clear_terminal(); // Draw outer border. - //box(stdscr, 0, 0); TODO DAM - // WIP + TUI.draw_box(1, 1, size_x, size_y); // Draw table grids. // TODO Maybe this could be simplified? @@ -979,13 +978,15 @@ draw_tui :: (db: *Database, layout: *Layout) { /////////////////////////////////////////////////////////////////////////// // Draw headers. - y = 0; - x = 0; + y = 1; + x = 1; // Headers : title x += 1; col = *layout.columns[L_TITLE_IDX]; //mvaddstr(xx y, xx (x + col.alignment_offset), ifx db == *archive then layout.archive_title.data else col.header.data); TODO DAM + TUI.set_cursor_position(y, x + col.alignment_offset); + write_string(ifx db == *archive then layout.archive_title else col.header); x += col.width; // Headers : days @@ -1003,6 +1004,8 @@ draw_tui :: (db: *Database, layout: *Layout) { col = *layout.columns[L_DAYS_IDX + idx]; //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM + TUI.set_cursor_position(y, x + col.alignment_offset); + write_string(col.header); x += col.width; // Reset theme. @@ -1013,6 +1016,8 @@ draw_tui :: (db: *Database, layout: *Layout) { x += 1; col = *layout.columns[L_TOTAL_IDX]; //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM + TUI.set_cursor_position(y, x + col.alignment_offset); + write_string(col.header); /////////////////////////////////////////////////////////////////////////// @@ -1021,7 +1026,7 @@ draw_tui :: (db: *Database, layout: *Layout) { total_time := 0; column_width: int; - y = 0; + y = 1; // Pagination based on currently selected task (show page where selected task is). idx_start := (db.selected_idx / layout_tasks_rows) * layout_tasks_rows; // Display up to rows allowed by the layout, or less if reached end of database. @@ -1030,7 +1035,7 @@ draw_tui :: (db: *Database, layout: *Layout) { auto_release_temp(); // TODO Temporary memory being trashed?! task := *db.tasks[task_idx]; y += 1; - x = 0; + x = 1; // Apply theme. if (task == active_task && task == selected_task) { @@ -1047,6 +1052,8 @@ draw_tui :: (db: *Database, layout: *Layout) { x += 1; column_width = layout.columns[L_TITLE_IDX].width; // mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. TODO DAM + TUI.set_cursor_position(y, x); + print("%", cast(string)task.name); // TODO Needs adjustment to fit column width. x += column_width; // Task times. @@ -1072,12 +1079,16 @@ draw_tui :: (db: *Database, layout: *Layout) { /////////////////////////////////////////////////////////////////////////// // Draw selected/total tasks. + // TODO Something is wrong here... sometimes it only changes to the small format on the 1001 instead of 1000. FIXME size := 1 + count_digits(db.selected_idx + 1) + 1 + count_digits(db.tasks.count) + 1; // " XXX/YYY " + TUI.set_cursor_position(size_y, 2); if (size <= layout.columns[L_TITLE_IDX].width) { // mvprintw(size_y - 1, 1, " %td/%zd ", db.selected_idx + 1, db.tasks.count); TODO DAM + print(" %/% ", db.selected_idx + 1, db.tasks.count); } else { // mvprintw(size_y - 1, 1, "%td", db.selected_idx + 1); TODO DAM + print("%", db.selected_idx + 1); } @@ -1274,7 +1285,13 @@ main :: () { case; { TUI.set_cursor_position(3+drop_down, 2); - write_string(TUI.to_string(key)); + str := TUI.to_string(key); + for 0..str.count-1 { + if str[it] == #char "\e" { + str[it] = #char "?"; + } + } + write_string(str); drop_down += 1; } } @@ -1291,10 +1308,6 @@ main :: () { print("size(CxR): %x%\n", xcolumns, xrows); } - #if true { - return; - } - // -- -- -- Testing TUI -- STOP @@ -1481,9 +1494,9 @@ main :: () { db := *database; layout := *layouts[Layouts.COMPACT]; - - //flushinp(); TODO DAM - TIO.tui_ungetch(KEY_RESIZE); // TODO DAM + + TUI.flush_input(); + TUI.set_next_key(TUI.Keys.Resize); while (true) { if (is_terminal_too_small) { @@ -1496,8 +1509,7 @@ main :: () { } reset_temporary_storage(); - //timeout(INPUT_TIMEOUT_MS); TODO DAM - key := TIO.tui_getch(); // getch(); TODO DAM + key := TUI.get_key(INPUT_TIMEOUT_MS); if key == #char "q" || key == #char "Q" break; update_times(*database); //timeout(INPUT_AWAIT_INF); TODO DAM @@ -1518,10 +1530,11 @@ main :: () { else ifx (selected_idx < 0) then 1 else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS; } + if key == { // When getch() times out. - case .READ_ERROR; + case TUI.Keys.None; if (is_autosave_enabled && countdown_to_autosave > 0) { countdown_to_autosave -= INPUT_TIMEOUT_MS; if (countdown_to_autosave <= 0) { @@ -1534,14 +1547,13 @@ main :: () { } // When terminal is resized. - case KEY_RESIZE; - TIO.tui_clear_screen(); + case TUI.Keys.Resize; + TUI.clear_terminal(); //getmaxyx(stdscr, *size_y, *size_x); - TIO.update_terminal_size(); // TODO DAM - size_x = TIO.terminal_state.width; - size_y = TIO.terminal_state.height; + _y, _x := TUI.get_terminal_size(); + size_y = xx _y; + size_x = xx _x; print("resize:%x%", size_x, size_y); - asd := TIO.tui_getch(); is_terminal_too_small = size_x < 60 || size_y < 3; update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; @@ -1853,19 +1865,19 @@ main :: () { case KEY_HOME; select_task(db, 0); - case KEY_UP; + case TUI.Keys.Up; select_task_by_delta(db, -1); - case KEY_PPAGE; + case TUI.Keys.PgUp; select_task_by_delta(db, -layout_tasks_rows); case KEY_END; select_task(db, db.tasks.count-1); - case KEY_DOWN; + case TUI.Keys.Down; select_task_by_delta(db, 1); - case KEY_NPAGE; + case TUI.Keys.PgDown; select_task_by_delta(db, layout_tasks_rows); } } @@ -1892,7 +1904,7 @@ main :: () { //getch(); TODO DAM } - TIO.terminate(); // TODO DAM + TUI.stop(); exit(xx ifx error_saving then 1 else 0); } -- cgit v1.2.3 From 6137ac4219b98c412f77ebc3c70ae196c13641ee Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 22 Dec 2023 10:30:44 +0000 Subject: Fixed bug in count_digits. Converted print_time to TUI. --- TUI/unix.jai | 1 - ttt.jai | 65 +++++++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 21 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/unix.jai b/TUI/unix.jai index 03a371b..a35c2dc 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -209,7 +209,6 @@ was_resized : bool; resize_handler :: (signal_code : s32) #c_call { new_context : Context; push_context new_context { - print("SIGNAL:%", signal_code); if signal_code != SIGWINCH then return; atomic_swap(*was_resized, true); } diff --git a/ttt.jai b/ttt.jai index 11bf9f4..c411d81 100644 --- a/ttt.jai +++ b/ttt.jai @@ -182,8 +182,13 @@ is_equal_to_any :: (to_compare :string, test_a :string, test_b :string) -> bool // Count digits required to represent number on base. Sign is discarded. count_digits :: (number: s64, base: s64 = 10) -> s64 { - if number == 0 return 1; - return cast(s64) floor( log(cast(float64)abs(number)) / log(cast(float64)abs(base)) ) + 1; + assert(base > 1, "The smallest integer base for a number system is 2."); + digits := 0; + while number != 0 { + number /= base; + digits += 1; + } + return digits; } Text_Encoding :: enum u8 #specified { @@ -250,7 +255,7 @@ is_empty_string :: (str: string) -> bool { // Prints, on row y and column x, the time using 5 characters centered on space. // Returns the result of a call to mvprintw. -mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { +print_time :: (y: int, x: int, time: s64, space: int) -> int { TIME_CHARS :: 5; assert(space >= TIME_CHARS); @@ -260,24 +265,42 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { left_padding := (space - TIME_CHARS) / 2; right_padding := space - TIME_CHARS - left_padding; + + print_padding :: (size: int, char: u8 = #char " ") { + assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience."); + while size > 0 { + print_character(char); + size -= 1; + } + } + + TUI.set_cursor_position(y, x); if time < 0 { + print_padding(left_padding); + write_string(" - "); + print_padding(right_padding); return 0; - // return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, ""); TODO DAM } else if time == 0 { + print_padding(left_padding); + write_string(" 0 "); + print_padding(right_padding); return 0; - // return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, ""); TODO DAM } 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; - // return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, ""); TODO DAM } 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; - // return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); TODO DAM } else if time < #run mul_f64_s64(9999.5, SECONDS_IN_DAY) { value := cast(float64) time / SECONDS_IN_DAY; @@ -285,8 +308,10 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { 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}); + print_padding(right_padding); return 0; - // return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); TODO DAM } else if time < #run mul_f64_s64(9999.5, SECONDS_IN_YEAR) { value := cast(float64) time / SECONDS_IN_YEAR; @@ -294,13 +319,16 @@ mvprintw_time :: (y: s32, x: s32, time: s64, space: s32) -> int { 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}); + print_padding(right_padding); return 0; - // return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); TODO DAM } else { - // TODO Set back the unicode emoji once ncurses has been replaced. + print_padding(left_padding); + write_string(" ∞ "); + print_padding(right_padding); return 0; - // return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, ""); TODO DAM } } @@ -1064,13 +1092,13 @@ draw_tui :: (db: *Database, layout: *Layout) { column_width = layout.columns[L_DAYS_IDX + day_idx].width; task_time := task.times[day_idx]; total_time = add(total_time, task_time); - mvprintw_time(xx y, xx x, task_time, xx column_width); + print_time(y, x, task_time, column_width); x += column_width; } // Task total. x += 1; - mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); + print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); // Reset theme. //attrset(A_NORMAL); TODO DAM @@ -1079,22 +1107,19 @@ draw_tui :: (db: *Database, layout: *Layout) { /////////////////////////////////////////////////////////////////////////// // Draw selected/total tasks. - // TODO Something is wrong here... sometimes it only changes to the small format on the 1001 instead of 1000. FIXME - size := 1 + count_digits(db.selected_idx + 1) + 1 + count_digits(db.tasks.count) + 1; // " XXX/YYY " + size := 1 + count_digits(db.selected_idx + 1) + 1 + count_digits(db.tasks.count) + 1; // " XXX/YYY " TUI.set_cursor_position(size_y, 2); if (size <= layout.columns[L_TITLE_IDX].width) { - // mvprintw(size_y - 1, 1, " %td/%zd ", db.selected_idx + 1, db.tasks.count); TODO DAM print(" %/% ", db.selected_idx + 1, db.tasks.count); } else { - // mvprintw(size_y - 1, 1, "%td", db.selected_idx + 1); TODO DAM print("%", db.selected_idx + 1); } /////////////////////////////////////////////////////////////////////////// // Draw daily totals. - y = size_y - 1; + y = size_y; x = 0 + 1 + layout.columns[L_TITLE_IDX].width; total_time = 0; for 0..NUM_WEEK_DAYS-1 { @@ -1112,14 +1137,14 @@ draw_tui :: (db: *Database, layout: *Layout) { column_width = layout.columns[L_DAYS_IDX + idx].width; total_time = add(total_time, daily_total); - mvprintw_time(xx y, xx x, daily_total, xx column_width); + print_time(y, x, daily_total, column_width); x += column_width; // Reset theme. //attrset(A_NORMAL); TODO DAM } x += 1; - mvprintw_time(xx y, xx x, total_time, xx layout.columns[L_TOTAL_IDX].width); + print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); } free_memory :: () { -- cgit v1.2.3 From 3d9e9a30f6ce5dee1775bdd1fbe28f07e41365b2 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 24 Dec 2023 01:13:08 +0000 Subject: Draw table and fixed totals position. --- TUI/module.jai | 16 ++++++++-------- ttt.jai | 39 ++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 23 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index bb5b800..9014be7 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -43,11 +43,11 @@ Drawings :: struct { } Commands :: struct { - EnterAlternateBuffer :: "\e[?1049h"; - EnterMainBuffer :: "\e[?1049l"; + StartAlternateBuffer :: "\e[?1049h"; + StartMainBuffer :: "\e[?1049l"; - EnterDrawingMode :: "\e(0"; - EnterNormalMode :: "\e(B"; + StartDrawingMode :: "\e(0"; + StartNormalMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; @@ -371,7 +371,7 @@ start :: () { input_string.count = 0; input_override = xx Keys.None; - write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.EnterAlternateBuffer, Commands.SetUTF8); + write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.StartAlternateBuffer, Commands.SetUTF8); OS_prepare_terminal(); initialized = true; @@ -382,7 +382,7 @@ stop :: () { initialized = false; OS_reset_terminal(); - write_strings(Commands.EnterMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); + write_strings(Commands.StartMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } flush_input :: () { @@ -402,7 +402,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { tmp_string = tprint(Commands.SetCursorPosition, y, x); write_strings( - Commands.EnterDrawingMode, + Commands.StartDrawingMode, tmp_string, Drawings.CornerTL ); @@ -431,7 +431,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { } write_string(Drawings.CornerBR); - write_string(Commands.EnterNormalMode); + write_string(Commands.StartNormalMode); } // TODO Maybe rename to "clear()" diff --git a/ttt.jai b/ttt.jai index c411d81..58a025b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -21,13 +21,9 @@ #import "System"; #import "Sort"; #import "Math"; -// #import "POSIX"; #import "File"; #import "File_Utilities"; #import "String"; -// #import "curses"; -// #import "kscurses"; -TIO :: #import "tio"; // TODO Move things into TUI. TUI :: #import "TUI"; #load "Integer_Saturating_Arithmetic.jai"; @@ -991,17 +987,22 @@ draw_tui :: (db: *Database, layout: *Layout) { // Draw table grids. // TODO Maybe this could be simplified? - y = 0; - x = 0; + y = 1; + x = 1; + write_string(TUI.Commands.StartDrawingMode); for 0..layout.columns.count-2 { column := layout.columns[it]; x += 1 + column.width; - //mvaddch(xx y, xx x, ACS_TTEE); TODO DAM - for row: 1..size_y-1 { - //mvaddch(xx row, xx x, ACS_VLINE); TODO DAM + TUI.set_cursor_position(y, x); + write_string(TUI.Drawings.TeeT); + for row: 2..size_y { + TUI.set_cursor_position(row, x); + write_string(TUI.Drawings.LineV); } - //mvaddch(size_y-1, xx x, ACS_BTEE); TODO DAM + TUI.set_cursor_position(size_y, x); + write_string(TUI.Drawings.TeeB); } + write_string(TUI.Commands.StartNormalMode); /////////////////////////////////////////////////////////////////////////// @@ -1081,7 +1082,15 @@ draw_tui :: (db: *Database, layout: *Layout) { column_width = layout.columns[L_TITLE_IDX].width; // mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. TODO DAM TUI.set_cursor_position(y, x); - print("%", cast(string)task.name); // TODO Needs adjustment to fit column width. + // TODO FIXME OH MY GOD SUCH BAD CODEZ + task_name: string = cast(string)task.name; + task_name.count = ifx task_name.count > column_width then column_width else task_name.count; + print("%", task_name); + diff := column_width - task_name.count; + while diff > 0 { + diff -= 1; + print_character(#char " "); + } x += column_width; // Task times. @@ -1120,7 +1129,7 @@ draw_tui :: (db: *Database, layout: *Layout) { /////////////////////////////////////////////////////////////////////////// // Draw daily totals. y = size_y; - x = 0 + 1 + layout.columns[L_TITLE_IDX].width; + x = 1 + 1 + layout.columns[L_TITLE_IDX].width; total_time = 0; for 0..NUM_WEEK_DAYS-1 { idx := adjust_first_day_of_week[it]; @@ -1526,7 +1535,8 @@ main :: () { if (is_terminal_too_small) { INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; - //mvaddstr(size_y / 2, (size_x - xx INVALID_WINDOW_MESSAGE.count) / 2, INVALID_WINDOW_MESSAGE); TODO DAM + TUI.set_cursor_position(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE.count) / 2); + write_strings(INVALID_WINDOW_MESSAGE); } else { draw_tui(db, layout); @@ -1574,11 +1584,10 @@ main :: () { // When terminal is resized. case TUI.Keys.Resize; TUI.clear_terminal(); - //getmaxyx(stdscr, *size_y, *size_x); _y, _x := TUI.get_terminal_size(); + // TODO FIX ME size_y = xx _y; size_x = xx _x; - print("resize:%x%", size_x, size_y); is_terminal_too_small = size_x < 60 || size_y < 3; update_layout(); layout = *layouts[ifx size_x > 100 then Layouts.NORMAL else Layouts.COMPACT]; -- cgit v1.2.3 From 2a20d715b56cf60f090b856ba6f1c98a3c87fe9f Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 24 Dec 2023 14:31:34 +0000 Subject: Starting to implement graphics styles. --- TUI/module.jai | 61 ++++++++++++++++++++++++++++++++++------------------------ ttt.jai | 8 ++++---- 2 files changed, 40 insertions(+), 29 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 9014be7..37c0f31 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -43,11 +43,11 @@ Drawings :: struct { } Commands :: struct { - StartAlternateBuffer :: "\e[?1049h"; - StartMainBuffer :: "\e[?1049l"; + AlternateScreenBuffer :: "\e[?1049h"; + MainScreenBuffer :: "\e[?1049l"; - StartDrawingMode :: "\e(0"; - StartNormalMode :: "\e(B"; + DrawingMode :: "\e(0"; + TextMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; @@ -59,6 +59,10 @@ Commands :: struct { SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE + // WIP WIP WIP + // Add commands to change StyleNormal/Italic/Bold/Color/WhatNot! + + // Cursor Position SetCursorPosition :: "\e[%;%H"; @@ -92,21 +96,30 @@ Commands :: struct { } -// TODO Maybe make the OS_* procedures as inline?! - -// 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). +GraphicsStyle :: struct { + BackgroundColor : u8; + ForegroundColor : u8; + Bold : bool; + Italic : bool; + Underline : bool; + Blinking : bool; + Inverse : bool; + StrikeThrough : bool; +} +// TODO Maybe make the OS_* procedures as inline?! /* - In this block, we're testing having the Key as a union of both [8]u8 and u64. - It kinda looks OK, but it may drive the users to extract info from the code while - it should be used only for comparison. + 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; KEY_SIZE :: #run type_info(Key).runtime_size; @@ -125,13 +138,6 @@ Keys :: struct { PgDown : Key : #run to_key("\e[6~"); } -/* TODO - 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|) -*/ - to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { k: Key; // #if DEBUG { @@ -371,7 +377,7 @@ start :: () { input_string.count = 0; input_override = xx Keys.None; - write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.StartAlternateBuffer, Commands.SetUTF8); + write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.AlternateScreenBuffer, Commands.SetUTF8); OS_prepare_terminal(); initialized = true; @@ -382,13 +388,18 @@ stop :: () { initialized = false; OS_reset_terminal(); - write_strings(Commands.StartMainBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); + write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } flush_input :: () { OS_flush_input(); } +// WIP WIP WIP +// set_style :: () { + // print("", Commands.) -- +// } + draw_box :: (x: int, y: int, width: int, height: int) { assert_is_initialized(); @@ -402,7 +413,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { tmp_string = tprint(Commands.SetCursorPosition, y, x); write_strings( - Commands.StartDrawingMode, + Commands.DrawingMode, tmp_string, Drawings.CornerTL ); @@ -431,7 +442,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { } write_string(Drawings.CornerBR); - write_string(Commands.StartNormalMode); + write_string(Commands.TextMode); } // TODO Maybe rename to "clear()" diff --git a/ttt.jai b/ttt.jai index 58a025b..b5dca56 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,7 +17,7 @@ // - release : jai ttt.jai -import_dir . -quiet -x64 -release // - debug : jai ttt.jai -import_dir . -quiet -x64 -#import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. +#import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. #import "System"; #import "Sort"; #import "Math"; @@ -48,7 +48,7 @@ WINDOW :: struct { } // TODO DAM VERSION :: "2.0"; // Use only 3 chars (to fit layouts). -YEAR :: "2023"; +YEAR :: "2024"; FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). NUM_WEEK_DAYS :: 7; // TODO This has to go - Just to be more clear about what we're looping about. NAME_SIZE :: 72; // TODO Use this instead of Task.name.count ? @@ -989,7 +989,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // TODO Maybe this could be simplified? y = 1; x = 1; - write_string(TUI.Commands.StartDrawingMode); + write_string(TUI.Commands.DrawingMode); for 0..layout.columns.count-2 { column := layout.columns[it]; x += 1 + column.width; @@ -1002,7 +1002,7 @@ draw_tui :: (db: *Database, layout: *Layout) { TUI.set_cursor_position(size_y, x); write_string(TUI.Drawings.TeeB); } - write_string(TUI.Commands.StartNormalMode); + write_string(TUI.Commands.TextMode); /////////////////////////////////////////////////////////////////////////// -- cgit v1.2.3 From 6dd12d2c8b81cd5719fb0ecdcbf678e08460fbeb Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 1 Jan 2024 18:42:24 +0000 Subject: Fixed bug that was clamping terminal sequences to 6 bytes. Added style formatting. --- TUI/module.jai | 269 +++++++++++++++++++++++++++++++++++++++++++++++++-------- ttt.jai | 47 +++++++--- 2 files changed, 266 insertions(+), 50 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 37c0f31..95ab8dc 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -50,18 +50,17 @@ Commands :: struct { TextMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; + ClearScrollBack :: "\e[3J"; Bell :: "\x07"; - SetWindowTitle :: "\e]0;%\x07"; + SetWindowTitle :: "\e]0;%\e\\"; RefreshWindow :: "\e[7t"; // TODO Not yet tested. SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE - // WIP WIP WIP - // Add commands to change StyleNormal/Italic/Bold/Color/WhatNot! - + SetGraphicsRendition :: "\e[%m"; // Cursor Position SetCursorPosition :: "\e[%;%H"; @@ -93,18 +92,44 @@ Commands :: struct { QueryCursorPosition :: "\e[6n"; // Emits the cursor position as: "ESC [ ; R" Where = row and = column. QueryDeviceAttributes :: "\e[0c"; QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. - } -GraphicsStyle :: struct { - BackgroundColor : u8; - ForegroundColor : u8; - Bold : bool; - Italic : bool; - Underline : bool; - Blinking : bool; - Inverse : bool; - StrikeThrough : bool; +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?! @@ -124,18 +149,187 @@ Key :: u64; KEY_SIZE :: #run type_info(Key).runtime_size; -Keys :: struct { +Keys :: struct #type_info_none { // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. - None : Key : 0xF0000000_0000000F; - Resize : Key : 0xF0000000_0000001F; + None : Key : 0xF0000000_0000000F; + Resize : Key : 0xF0000000_0000001F; - 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"); - - PgUp : Key : #run to_key("\e[5~"); - PgDown : Key : #run to_key("\e[6~"); + 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~"); + + 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("\eO1P"); + MetaF2 : Key : #run to_key("\eO1Q"); + MetaF3 : Key : #run to_key("\eO1R"); + MetaF4 : Key : #run to_key("\eO1S"); + 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("\eO2P"); + ShiftF2 : Key : #run to_key("\eO2Q"); + ShiftF3 : Key : #run to_key("\eO2R"); + ShiftF4 : Key : #run to_key("\eO2S"); + 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("\eO3P"); + AltF2 : Key : #run to_key("\eO3Q"); + AltF3 : Key : #run to_key("\eO3R"); + AltF4 : Key : #run to_key("\eO3S"); + 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("\eO4P"); + AltShiftF2 : Key : #run to_key("\eO4Q"); + AltShiftF3 : Key : #run to_key("\eO4R"); + AltShiftF4 : Key : #run to_key("\eO4S"); + 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("\eO5P"); + CtrlF2 : Key : #run to_key("\eO5Q"); + CtrlF3 : Key : #run to_key("\eO5R"); + CtrlF4 : Key : #run to_key("\eO5S"); + 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("\eO6P"); + CtrlShiftF2 : Key : #run to_key("\eO6Q"); + CtrlShiftF3 : Key : #run to_key("\eO6R"); + CtrlShiftF4 : Key : #run to_key("\eO6S"); + 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("\eO7P"); + CtrlAltF2 : Key : #run to_key("\eO7Q"); + CtrlAltF3 : Key : #run to_key("\eO7R"); + CtrlAltF4 : Key : #run to_key("\eO7S"); + 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("\eO8P"); + CtrlAltShiftF2 : Key : #run to_key("\eO8Q"); + CtrlAltShiftF3 : Key : #run to_key("\eO8R"); + CtrlAltShiftF4 : Key : #run to_key("\eO8S"); + 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; } { @@ -160,6 +354,7 @@ to_string :: inline (key: Key) -> string { return str; } +// TODO FIXME DEBUG HACK test_union :: () { // ti := type_info(K); @@ -209,8 +404,9 @@ input_string : string; input_override : Key; #run { + // TODO FIXME DEBUG HACK or maybe... let it be?! // Some tests. - assert(input_buffer.count >= 6, "The input buffer size must be capable to hold an entire terminal (6 bytes) or UTF8 (4 bytes) code."); + 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 () { @@ -314,7 +510,8 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // Must be a terminal escape sequence. if utf8_bytes == 1 && input_string[0] == #char "\e" { - to_parse.count = ifx input_string.count > 6 then 6 else input_string.count; // TODO We should look into the input_string and search for the following escape sequence or somehting!? + 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); @@ -345,13 +542,11 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return xx Keys.None; } -get_str :: (count_limit: int = -1, allocator: Allocator = temp) -> string { +get_str :: (count_limit: int = -1) -> string { assert_is_initialized(); - assert(allocator.proc != null, "Argument 'allocator.proc' has invalid null value."); if count_limit < 0 { - builder: String_Builder(); - builder.allocator = allocator; + builder: String_Builder; init_string_builder(*builder); while(1) { @@ -361,10 +556,10 @@ get_str :: (count_limit: int = -1, allocator: Allocator = temp) -> string { if buffer.count < buffer.allocated break; expand(*builder); } - return builder_to_string(*builder, allocator); + return builder_to_string(*builder); } else { - buffer := alloc_string(count_limit, allocator); + buffer := alloc_string(count_limit); buffer.count = OS_read_input(buffer.data, count_limit); return buffer; } @@ -395,7 +590,7 @@ flush_input :: () { OS_flush_input(); } -// WIP WIP WIP +// TODO move style related procedures here! // set_style :: () { // print("", Commands.) -- // } @@ -463,7 +658,7 @@ get_terminal_size :: () -> rows: int, columns: int { flush_input(); write_string(Commands.QueryWindowSizeInChars); - input := get_str(64); + input := get_str(64,, temporary_allocator); // Expected response format: \e[8;;t // where is the number of rows and of columns. @@ -483,7 +678,7 @@ get_terminal_size :: () -> rows: int, columns: int { 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, ";"); + parts := split(input, ";",, temporary_allocator); rows = parse_int(*parts[1]); columns = parse_int(*parts[2]); } @@ -504,7 +699,7 @@ get_cursor_position :: () -> row: int, column: int { flush_input(); write_string(Commands.QueryCursorPosition); - input := get_str(64); + input := get_str(64,, temporary_allocator); // Expected response format: \e[;R // where is the number of rows and of columns. @@ -526,7 +721,7 @@ get_cursor_position :: () -> row: int, column: int { advance(*input, 2); - parts := split(input, ";"); + parts := split(input, ";",, temporary_allocator); row := parse_int(*parts[0]); column := parse_int(*parts[1]); return row, column; diff --git a/ttt.jai b/ttt.jai index b5dca56..2e5af82 100644 --- a/ttt.jai +++ b/ttt.jai @@ -392,7 +392,7 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us 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); + 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; @@ -713,7 +713,7 @@ import_from_csv :: (db: *Database, path: string) -> bool { // Thus this works on both 'dos' and 'unix'-style files. s := << sp; - found, result, right := split_from_left(s, 10); + found, result, right := split_from_left(s, 10,, temporary_allocator); if !found { // This is the last line; there may not have been a linefeed after that, @@ -761,7 +761,7 @@ import_from_csv :: (db: *Database, path: string) -> bool { if success == false break; task: Task; - csv_values := split(line, ","); // TODO Temporary memory... if file is too big this may break. + csv_values := split(line, ",",, temporary_allocator); // TODO Temporary memory... if file is too big this may break. // Import task name. name_length := min(task.name.count, csv_values[0].count); @@ -1069,11 +1069,16 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (task == active_task && task == selected_task) { // attron(COLOR_PAIR(xx Styles.ACTIVE_SELECTED) | A_BOLD); TODO DAM + TUI.set_style_colors(TUI.Colors8b.Red, 6); // TODO TESTING COLORS + TUI.set_style(true, true, true, false); } else if (task == selected_task) { // attron(COLOR_PAIR(xx Styles.SELECTED)); TODO DAM + TUI.set_style_colors(4, 6); // TODO TESTING COLORS } else if (task == active_task) { + TUI.set_style_colors(TUI.Colors8b.Red, 6); // TODO TESTING COLORS + TUI.set_style(false, false, false, true); // TODO TESTING STYLE // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM } @@ -1081,16 +1086,18 @@ draw_tui :: (db: *Database, layout: *Layout) { x += 1; column_width = layout.columns[L_TITLE_IDX].width; // mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. TODO DAM - TUI.set_cursor_position(y, x); // TODO FIXME OH MY GOD SUCH BAD CODEZ + TUI.set_cursor_position(y, x); + white_spaces := column_width; + // FIXME Improve by using buffer instead of printing one char at the time. + while white_spaces > 0 { + print_character(#char " "); + white_spaces -= 1; + } + TUI.set_cursor_position(y, x); task_name: string = cast(string)task.name; task_name.count = ifx task_name.count > column_width then column_width else task_name.count; print("%", task_name); - diff := column_width - task_name.count; - while diff > 0 { - diff -= 1; - print_character(#char " "); - } x += column_width; // Task times. @@ -1111,6 +1118,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Reset theme. //attrset(A_NORMAL); TODO DAM + TUI.clear_style(); } @@ -1302,7 +1310,7 @@ main :: () { last_none_char := "X"; drop_down := 0; while(key != #char "q") { - // __mark := get_temporary_storage_mark(); + __mark := get_temporary_storage_mark(); if key == { case TUI.Keys.None; { @@ -1316,16 +1324,27 @@ main :: () { TUI.clear_terminal(); drop_down = 0; } + + case TUI.Keys.MetaF7; { + TUI.set_cursor_position(3+drop_down, 2); + drop_down += 1; + write_string("META F7"); + } case; { TUI.set_cursor_position(3+drop_down, 2); str := TUI.to_string(key); + 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(" Date: Mon, 1 Jan 2024 19:15:55 +0000 Subject: Added some TODO items and fixed show_processing procedure. --- TUI/module.jai | 17 ++++++++++++++--- ttt.jai | 8 ++++---- 2 files changed, 18 insertions(+), 7 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 95ab8dc..8c1d7ed 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -542,7 +542,8 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return xx Keys.None; } -get_str :: (count_limit: int = -1) -> string { +// TODO Maybe rename!? Or replace with read_string... or read_input... or something else?! +get_string :: (count_limit: int = -1) -> string { assert_is_initialized(); if count_limit < 0 { @@ -565,6 +566,16 @@ get_str :: (count_limit: int = -1) -> string { } } +read_string :: (count_limit: int = -1) -> string, Key { + // TODO WIP FIXME WIP + /* + 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 be used to end the input. + Escape should discard the input returning an empty string and a Enter key. + Resize should discard the input returning an empty string and a Resize key. + */ +} + start :: () { if initialized == true return; @@ -658,7 +669,7 @@ get_terminal_size :: () -> rows: int, columns: int { flush_input(); write_string(Commands.QueryWindowSizeInChars); - input := get_str(64,, temporary_allocator); + input := get_string(64,, temporary_allocator); // Expected response format: \e[8;;t // where is the number of rows and of columns. @@ -699,7 +710,7 @@ get_cursor_position :: () -> row: int, column: int { flush_input(); write_string(Commands.QueryCursorPosition); - input := get_str(64,, temporary_allocator); + input := get_string(64,, temporary_allocator); // Expected response format: \e[;R // where is the number of rows and of columns. diff --git a/ttt.jai b/ttt.jai index 2e5af82..62c2e6c 100644 --- a/ttt.jai +++ b/ttt.jai @@ -167,8 +167,9 @@ trigger_autosave :: () { } show_processing :: () { - //mvaddch(0, 0, ACS_DIAMOND); TODO DAM - //refresh(); TODO DAM + TUI.set_cursor_position(1, 1); + // TODO Maybe add some color? + write_strings(TUI.Commands.DrawingMode, TUI.Drawings.Diamond, TUI.Commands.TextMode); } // Returns true if string to_compare is equal to any of the other passed strings, false otherwise. @@ -1955,8 +1956,7 @@ main :: () { if (error_saving) { print_error("Press any key to close."); draw_error_window(); - //timeout(INPUT_AWAIT_INF); TODO DAM - //getch(); TODO DAM + TUI.get_key(); } TUI.stop(); -- cgit v1.2.3 From 698836ed1c6a9175903edd41f4cafdf7cb4b946d Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 2 Jan 2024 01:48:31 +0000 Subject: Initial implementation of procedure to read user input from a single line. --- TUI/module.jai | 39 ++++++++++++++++++++++++++++++++------- ttt.jai | 31 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 8c1d7ed..4e87589 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -11,14 +11,17 @@ #import "Thread"; Drawings :: struct { - // TODO Maybe just use unicode instead of jumping back and forth between drawing mode?! + /* + TODO + Using unicode allows to just-print instead of jumping back and forth between drawing/text modes. + But it seems that not all terminals support unicode?! Do we even bother with those terminals?! + */ // test_drawing_without_mode :: () { // top := "┌───┐"; // btm := "└───┘"; // print(">%<\n", top); // print(">%<\n", btm); // } - // #run test_drawing_without_mode(); CornerBR :: "\x6A"; CornerTR :: "\x6B"; CornerTL :: "\x6C"; @@ -129,7 +132,10 @@ set_style_colors :: (foreground: u8, background: u8) { 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); + 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?! @@ -566,14 +572,33 @@ get_string :: (count_limit: int = -1) -> string { } } -read_string :: (count_limit: int = -1) -> string, Key { - // TODO WIP FIXME WIP +// TODO UNTESTED +user_line_input :: (count_limit: int = -1, is_visible: bool = true) -> string, Key { + + // TODO Show blinking cursor + row, col := get_cursor_position(); + + key := Keys.None; + builder: String_Builder; + init_string_builder(*builder); + + while key != Keys.Resize && key != Keys.Escape { + // TODO How to alloc/release temporary memory here? + key = get_key(); + if key == Keys.Enter then break; + str := to_string(key); + print("%", str); + print_to_builder(*builder, "%", str); + } + /* 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 be used to end the input. - Escape should discard the input returning an empty string and a Enter key. + 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 ""; + return result, key; } start :: () { diff --git a/ttt.jai b/ttt.jai index 62c2e6c..4e63204 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1364,6 +1364,37 @@ main :: () { print("size(CxR): %x%\n", xcolumns, xrows); } + if 1 { + print("TEST : user input --\n", to_standard_error = true); + auto_release_temp(); + TUI.start(); + TUI.clear_terminal(); + TUI.set_cursor_position(1, 1); + print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); + TUI.set_cursor_position(2, 1); + str, key := TUI.user_line_input(); + TUI.set_cursor_position(3, 1); + if key == { + case TUI.Keys.Escape; { + print("Have you pressed Esc? (y/n)"); + assert(TUI.get_key() == #char "y", "Failed to read line on Esc."); + print("> success\n", to_standard_error = true); + } + + case TUI.Keys.Resize; { + print("Have you resized the terminal? (y/n)"); + assert(TUI.get_key() == #char "y", "Failed to read line on resize."); + print("> success\n", to_standard_error = true); + } + case; { + print("Have you entered '%'? (y/n)", str); + assert(TUI.get_key() == #char "y", "Failed to read line."); + print("> success\n", to_standard_error = true); + } + } + TUI.stop(); + } + // -- -- -- Testing TUI -- STOP -- cgit v1.2.3 From 37f3a0f812f0b408c323c7c240197ec3c3a1e2f7 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 3 Jan 2024 01:47:04 +0000 Subject: WIP - implementing user line input. --- TUI/module.jai | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- ttt.jai | 12 ++++----- 2 files changed, 74 insertions(+), 17 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 4e87589..73f9260 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -71,8 +71,8 @@ Commands :: struct { // Cursor Visibility ShowCursor :: "\e[?25h"; HideCursor :: "\e[?25l"; - StartBlinking :: "\e[?25h]"; - StopBlinking :: "\e[?25l]"; + StartBlinking :: "\e[?25h"; + StopBlinking :: "\e[?25l"; SaveCursorPosition :: "\e7"; RestoreCursorPosition :: "\e8"; @@ -349,7 +349,7 @@ to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } return k; } -to_string :: inline (key: Key) -> string { +to_string :: inline (key: Key) -> string { // TODO FIXME TEMPORARY MEMORY str := talloc_string(KEY_SIZE); str.count = 0; while key != 0 #no_abc { @@ -360,6 +360,15 @@ to_string :: inline (key: Key) -> string { return str; } +is_escape_code :: inline (key: Key) -> bool { + result := false; + while key != 0 { + key >>= 8; + result |= ((key ^ Keys.Escape) == 0); + } + return result; +} + // TODO FIXME DEBUG HACK test_union :: () { @@ -573,31 +582,79 @@ get_string :: (count_limit: int = -1) -> string { } // TODO UNTESTED -user_line_input :: (count_limit: int = -1, is_visible: bool = true) -> string, Key { +// TODO Is count_limit the number of bytes of UTF8 symbols? +user_line_input :: (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; - builder: String_Builder; - init_string_builder(*builder); + + // 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 == Keys.Enter then break; - str := to_string(key); - print("%", str); - print_to_builder(*builder, "%", str); + + 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 builder_to_string(*builder) else ""; + result := ifx key == Keys.Enter then str else ""; return result, key; } diff --git a/ttt.jai b/ttt.jai index 4e63204..654cde4 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1239,7 +1239,7 @@ main :: () { // TODO Test input - if 1 { + if 0 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); ROW :: 3; @@ -1251,7 +1251,7 @@ main :: () { print("> success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : test key input --\n", to_standard_error = true); auto_release_temp(); TUI.start(); @@ -1271,7 +1271,7 @@ main :: () { print("> success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : draw box --\n", to_standard_error = true); auto_release_temp(); TUI.start(); @@ -1285,7 +1285,7 @@ main :: () { print("> success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : get terminal size --\n", to_standard_error = true); auto_release_temp(); TUI.start(); @@ -1303,7 +1303,7 @@ main :: () { } #if 1 { - print("test 5\n", to_standard_error = true); + print("TEST : print keys and set terminal title --\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); xcolumns, xrows: int; @@ -1372,7 +1372,7 @@ main :: () { TUI.set_cursor_position(1, 1); print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); TUI.set_cursor_position(2, 1); - str, key := TUI.user_line_input(); + str, key := TUI.user_line_input(15); TUI.set_cursor_position(3, 1); if key == { case TUI.Keys.Escape; { -- cgit v1.2.3 From e5d8eaa14407608a15a639da14bbea99dd8ef61a Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Jan 2024 01:28:21 +0000 Subject: Fixed Windows raw IO modes. --- TUI/module.jai | 62 +++++++++++++------------ TUI/windows.jai | 140 +++++++++++++++++++++++++++++++++++--------------------- ttt.jai | 4 +- 3 files changed, 122 insertions(+), 84 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index df88f0c..21f1dd7 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -667,7 +667,13 @@ start :: () { input_string.count = 0; input_override = xx Keys.None; - write_strings(Commands.HideCursor, Commands.SaveCursorPosition, Commands.AlternateScreenBuffer, Commands.SetUTF8); + write_strings( + Commands.HideCursor, + Commands.SaveCursorPosition, + Commands.AlternateScreenBuffer, + Commands.SetUTF8, + Commands.CursorNormalMode, + Commands.KeypadNumMode); OS_prepare_terminal(); initialized = true; @@ -744,39 +750,37 @@ clear_terminal :: inline () { // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { assert_is_initialized(); + + auto_release_temp(); + rows, columns: int = ---; - #if OS == .WINDOWS { - rows, columns = OS_get_terminal_size(); - } - else { - auto_release_temp(); - flush_input(); - write_string(Commands.QueryWindowSizeInChars); - input := get_string(64,, temporary_allocator); - - // Expected response format: \e[8;;t - // where is the number of rows and of columns. - FORMAT :: "\e[8;;t"; - - // Discard head noise. - while input.count >= 3 && (input[0] != FORMAT[0] || input[1] != FORMAT[1] || input[2] != FORMAT[2]) { - advance(*input); - } + flush_input(); + write_string(Commands.QueryWindowSizeInChars); + input := get_string(64,, temporary_allocator); - // 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."); + // Expected response format: \e[8;;t + // where is the number of rows and of columns. + FORMAT :: "\e[8;;t"; + + // Discard head noise. + while input.count >= 3 && (input[0] != FORMAT[0] || input[1] != FORMAT[1] || input[2] != FORMAT[2]) { + advance(*input); + } - parts := split(input, ";",, temporary_allocator); - rows = parse_int(*parts[1]); - columns = parse_int(*parts[2]); + // 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]); + return rows, columns; } diff --git a/TUI/windows.jai b/TUI/windows.jai index ed8c996..f704262 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -4,33 +4,42 @@ #import "System"; #import "Windows"; -// 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 - + // https://learn.microsoft.com/windows/win32/winprog/windows-data-types + LPVOID :: *void; + LPDWORD :: *s32; + BOOL :: bool; + SHORT :: s16; + WORD :: u16; + DWORD :: s32; +// 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"; + kernel32 :: #system_library "kernel32"; - // https://learn.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo + // 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/readconsole - ReadConsoleA :: (hConsoleInput: HANDLE, lpBuffer: *u8, nNumberOfCharsToRead: s32, lpNumberOfCharsRead: *s32, pInputControl := *void) -> bool #foreign kernel32; + // https://learn.microsoft.com/windows/console/readconsole + ReadConsoleA :: (hConsoleInput: HANDLE, lpBuffer: LPVOID, nNumberOfCharsToRead: DWORD, lpNumberOfCharsRead: LPVOID, pInputControl := LPVOID) -> bool #foreign kernel32; - // https://learn.microsoft.com/en-us/windows/console/getconsolemode - GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *u32) -> bool #foreign kernel32; + // https://learn.microsoft.com/windows/console/getconsolemode + GetConsoleMode :: (hConsoleHandle: HANDLE, lpMode: *DWORD) -> BOOL #foreign kernel32; - // https://learn.microsoft.com/en-us/windows/console/setconsolemode - SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: u32) -> bool #foreign kernel32; + // https://learn.microsoft.com/windows/console/setconsolemode + SetConsoleMode :: (hConsoleHandle: HANDLE, dwMode: DWORD) -> BOOL #foreign kernel32; - // https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-getlasterror + // 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-waitforsingleobjectex + WaitForSingleObjectEx :: (hHandle: HANDLE, dwMilliseconds: DWORD, bAlertable: BOOL) -> s32 #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 { - _UNUSED_0001_; + 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. _UNUSED_0008_; @@ -45,6 +54,7 @@ } // 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; // @@ -56,12 +66,6 @@ _UNUSED_0080_; } - - // https://learn.microsoft.com/en-us/windows/win32/winprog/windows-data-types - SHORT :: s16; - WORD :: u16; - DWORD :: s32; - COORD :: struct { X : SHORT; Y : SHORT; @@ -85,36 +89,69 @@ stdin: HANDLE; initial_stdin_mode: u32; - default_stdin_mode: Console_Input_Mode; - blocking_stdin_mode: Console_Input_Mode; - unblocking_stdin_mode: Console_Input_Mode; + raw_stdin_mode: Console_Input_Mode; stdout: HANDLE; initial_stdout_mode: u32; - default_stdout_mode: Console_Output_Mode; + raw_stdout_mode: Console_Output_Mode; -#scope_export +//////////////////////////////////////////////////////////////////////////////// +// Resize detection -OS_prepare_terminal :: () { - print("TODO TUI\n", to_standard_error = true); +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); + */ +} + +//////////////////////////////////////////////////////////////////////////////// + +#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 GetConsoleMode(stdin, *initial_stdin_mode) == false { + if xx GetConsoleMode(stdin, *initial_stdin_mode) == false { print("Failed to get input mode.", to_standard_error = true); return; } - default_stdin_mode = (cast(Console_Input_Mode) initial_stdin_mode) | .ENABLE_VIRTUAL_TERMINAL_INPUT; - blocking_stdin_mode = default_stdin_mode | .ENABLE_LINE_INPUT | .ENABLE_ECHO_INPUT; - unblocking_stdin_mode = default_stdin_mode & ~.ENABLE_LINE_INPUT & ~.ENABLE_ECHO_INPUT; + 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 default_stdin_mode) == false { + if SetConsoleMode(stdin, xx raw_stdin_mode) == false { print("Failed to set input mode: %.", GetLastError(), to_standard_error = true); return; } @@ -125,24 +162,25 @@ OS_prepare_terminal :: () { print("Invalid output handler.", to_standard_error = true); return; } - if GetConsoleMode(stdout, *initial_stdout_mode) == false { + if xx GetConsoleMode(stdout, *initial_stdout_mode) == false { print("Failed to get output mode.", to_standard_error = true); return; } - default_stdout_mode = (cast(Console_Output_Mode) initial_stdout_mode) | .ENABLE_PROCESSED_OUTPUT| .ENABLE_VIRTUAL_TERMINAL_PROCESSING; + 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 default_stdout_mode) == false { + if SetConsoleMode(stdout, xx raw_stdout_mode) == false { print("Failed to set output mode: %.", GetLastError(), to_standard_error = true); return; } } OS_reset_terminal :: () { - if SetConsoleMode(stdin, initial_stdin_mode) == false { + if xx SetConsoleMode(stdin, initial_stdin_mode) == false { print("Failed to reset input mode: %.", GetLastError(), to_standard_error = true); return; } - if SetConsoleMode(stdout, initial_stdout_mode) == false { + if xx SetConsoleMode(stdout, initial_stdout_mode) == false { print("Failed to reset output mode: %.", GetLastError(), to_standard_error = true); return; } @@ -157,16 +195,6 @@ OS_flush_input :: inline () { */ } -OS_get_terminal_size :: () -> rows: int, columns: int { - - ScreenBufferInfo: CONSOLE_SCREEN_BUFFER_INFO; - GetConsoleScreenBufferInfo(stdout, *ScreenBufferInfo); - columns := ScreenBufferInfo.srWindow.Right - ScreenBufferInfo.srWindow.Left + 1; - rows := ScreenBufferInfo.srWindow.Bottom - ScreenBufferInfo.srWindow.Top + 1; - - return rows, columns; -} - 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; @@ -181,14 +209,20 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo OS_wait_for_input :: (timeout_milliseconds: s32) -> is_input_available: bool { /* 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/win32/api/synchapi/nf-synchapi-waitforsingleobjectex */ - 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. + + poll_return := WaitForSingleObjectEx(stdin, timeout_milliseconds, true); error_code, error_message := get_error_value_and_string(); // FIXME Not used. - return ifx poll_return > 0 then true else false; + + // Possible values for poll_return TODO NOT BEING USED + WAIT_ABANDONED :: 0x00000080; + WAIT_IO_COMPLETION :: 0x000000C0; + WAIT_OBJECT_0 :: 0x00000000; + WAIT_TIMEOUT :: 0x00000102; + WAIT_FAILED :: 0xFFFFFFFF; + + return ifx poll_return == 0 then true else false; } OS_was_terminal_resized :: () -> bool { diff --git a/ttt.jai b/ttt.jai index 654cde4..4540699 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1239,7 +1239,7 @@ main :: () { // TODO Test input - if 0 { + if 1 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); ROW :: 3; @@ -1251,7 +1251,7 @@ main :: () { print("> success\n", to_standard_error = true); } - if 0 { + if 1 { print("TEST : test key input --\n", to_standard_error = true); auto_release_temp(); TUI.start(); -- cgit v1.2.3 From 0e9f04df9c1804b73baecdf4c3ad14fff977e2f1 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 11 Jan 2024 01:29:44 +0000 Subject: Very small fixes. --- TUI/unix.jai | 3 +++ ttt.jai | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/TUI/unix.jai b/TUI/unix.jai index dd53921..7a25b6b 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -266,6 +266,9 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo return bytes_read; } +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely OS_wait_for_input :: (timeout_milliseconds: s32) -> is_input_available: bool { fds := pollfd.[ .{ fd = STDIN_FILENO, events = POLLIN, revents = 0 } ]; nfds := fds.count; diff --git a/ttt.jai b/ttt.jai index 4540699..3e95802 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1263,7 +1263,7 @@ main :: () { while(key != #char "q") { __mark := get_temporary_storage_mark(); key = TUI.get_key(1000); - if key >= 33 && key <= 128 then print_character(cast,force(u8)key); + if key >= 32 && key <= 128 then print_character(cast,force(u8)key); else write_string("-"); set_temporary_storage_mark(__mark); } -- cgit v1.2.3 From e8ddaf3289a836c10f45b1b8c019922a99393488 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 9 Feb 2024 01:18:03 +0000 Subject: Reimplemented procedure to read input. --- TUI/module.jai | 129 +++++++++++++++++++++++++++++++++++++++++---------------- TUI/unix.jai | 3 +- ttt.jai | 7 +++- 3 files changed, 102 insertions(+), 37 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 21f1dd7..de56ee8 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -559,33 +559,55 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return xx Keys.None; } -// TODO Maybe rename!? Or replace with read_string... or read_input... or something else?! -get_string :: (count_limit: int = -1) -> string { +// 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 && terminators.count == 0 { + // TODO Write error to log. + // return ""; + // } + if count_limit < 0 { builder: String_Builder; init_string_builder(*builder); - while(1) { + while read_loop := true { 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 < buffer.allocated break; - expand(*builder); + buffer.count += OS_read_input(buffer_data + buffer.count, buffer.allocated - buffer.count); + for 0..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 = OS_read_input(buffer.data, count_limit); + buffer.count = 0; + + while read_loop := true { + buffer.count += OS_read_input(buffer.data + buffer.count, count_limit - buffer.count); + if buffer.count == count_limit then break; + for 0..buffer.count-1 { + for t: terminators { + if buffer[it] == t then break read_loop; + } + } + OS_wait_for_input(); + } + return buffer; } } // TODO UNTESTED -// TODO Is count_limit the number of bytes of UTF8 symbols? -user_line_input :: (count_limit: int, is_visible: bool = true) -> string, Key { +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); @@ -689,6 +711,8 @@ stop :: () { flush_input :: () { OS_flush_input(); + input_string.data = input_buffer.data; + input_string.count = 0; } // TODO move style related procedures here! @@ -753,33 +777,46 @@ get_terminal_size :: () -> rows: int, columns: int { auto_release_temp(); - rows, columns: int = ---; - flush_input(); write_string(Commands.QueryWindowSizeInChars); - input := get_string(64,, temporary_allocator); - - // Expected response format: \e[8;;t - // where is the number of rows and of columns. - FORMAT :: "\e[8;;t"; - // Discard head noise. - while input.count >= 3 && (input[0] != FORMAT[0] || input[1] != FORMAT[1] || input[2] != FORMAT[2]) { - advance(*input); - } + rows, columns: int = ---; + if OS_wait_for_input(0) { - // 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."); + // Expected response format: \e[8;;t + // where is the number of rows and of columns. + FORMAT :: "\e[8;;t"; + input := read_input(64, #char "t",, temporary_allocator); - parts := split(input, ";",, temporary_allocator); - rows = parse_int(*parts[1]); - columns = parse_int(*parts[2]); + // 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 { + #import "Math"; // TODO Maybe use S16_MAX values directly. + flush_input(); + cursor_row, cursor_column := get_cursor_position(); + defer set_cursor_position(cursor_row, cursor_column); + + set_cursor_position(S16_MAX, S16_MAX); + rows, columns = get_cursor_position(); + } return rows, columns; } @@ -798,12 +835,12 @@ get_cursor_position :: () -> row: int, column: int { flush_input(); write_string(Commands.QueryCursorPosition); - input := get_string(64,, temporary_allocator); // Expected response format: \e[;R // where is the number of rows and of columns. FORMAT :: "\e[;R"; - + input := read_input(64, #char "R"); + // Discard head noise. while input.count >= 2 && (input[0] != FORMAT[0] || input[1] != FORMAT[1]) { advance(*input); @@ -818,7 +855,6 @@ get_cursor_position :: () -> row: int, column: int { 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]); @@ -832,6 +868,29 @@ set_terminal_title :: (title: string) { } +test :: () { + + // A) Testing stuff +#if true { + start(); + flush_input(); + set_cursor_position(S16_MAX, S16_MAX); + r_a, c_a := get_cursor_position(); + stop(); + print("\n\rA) size: %, %\n", r_a, c_a); +} + + // B) Built way +#if true { + start(); + r_b, c_b := get_terminal_size(); + stop(); + print("\n\rB) size: %, %\n", r_b, c_b); +} + +} + + #if OS == .WINDOWS { // Prototyping zone... keep clear! } diff --git a/TUI/unix.jai b/TUI/unix.jai index 7a25b6b..2da4437 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -257,6 +257,7 @@ OS_flush_input :: inline () { 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 { @@ -269,7 +270,7 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo // timeout_milliseconds // 0: do not wait // -1: wait indefinitely -OS_wait_for_input :: (timeout_milliseconds: s32) -> is_input_available: bool { +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. diff --git a/ttt.jai b/ttt.jai index 3e95802..d4a5320 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1239,6 +1239,11 @@ main :: () { // TODO Test input + { + TUI.test(); + exit(0); + } + if 1 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); @@ -1372,7 +1377,7 @@ main :: () { TUI.set_cursor_position(1, 1); print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); TUI.set_cursor_position(2, 1); - str, key := TUI.user_line_input(15); + str, key := TUI.read_input_line(15); TUI.set_cursor_position(3, 1); if key == { case TUI.Keys.Escape; { -- cgit v1.2.3 From 4aa3ae769bc079dd5cbd3419cc44b1d95986f837 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 10 Feb 2024 02:53:54 +0000 Subject: Fixed some windows OS_ procedures. Simplified and fixed get_key(). --- TUI/module.jai | 228 ++++++++++++++++---------------------------------------- TUI/unix.jai | 1 + TUI/windows.jai | 180 ++++++++++++++++++++++++++++++++++++++++---- ttt.jai | 23 +++--- 4 files changed, 244 insertions(+), 188 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index de56ee8..64567fa 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -1,9 +1,13 @@ -#if OS == .WINDOWS { - #load "windows.jai"; -} else #if (OS == .LINUX) || (OS == .MACOS) { - #load "unix.jai"; -} else { - #assert(false, "Unsupported OS."); +// 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"; @@ -61,7 +65,8 @@ Commands :: struct { RefreshWindow :: "\e[7t"; // TODO Not yet tested. - SetUTF8 :: "\e%G"; // TODO TEST ME PLEASE + SetIEC2022 :: "\e%@"; + SetUTF8 :: "\e%G"; SetGraphicsRendition :: "\e[%m"; @@ -139,6 +144,7 @@ set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, } // 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. @@ -369,57 +375,13 @@ is_escape_code :: inline (key: Key) -> bool { return result; } -// TODO FIXME DEBUG HACK -test_union :: () -{ - // ti := type_info(K); - print("\n---\n%\n---\n", type_info(Key).*); - print("\n---\n%\n---\n", type_info(Key).runtime_size); - a: Key; - b: u8; - print(">%\n", a == b); - print(">%\n", a != Keys.None); - - c: string = ""; - print(">%\n", a == to_key(c)); - - d: []u8 = xx ""; - print(">%\n", a == to_key(d)); - - top := "┌───┐"; - btm := "└───┘"; - print(">%<\n", top); - print(">%<\n", btm); - - str := "abcd"; - tok := to_key(str); - tos := to_string(tok); - - print("1:%:\n", str); - print("2:"); - for 0..7 { - val := tok & 0xFF; - tok >>=8 ; - print("% ", FormatInt.{value=val, base=16, minimum_digits=2}); - } - print(":\n"); - print("3:%:\n", tos); - -} -#run test_union(); - -// Terminal action codes are encoded with values incompatible with UTF-8 to avoid collisions. - - initialized := false; -// input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! +//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; -was_resized : bool; - #run { // TODO FIXME DEBUG HACK or maybe... let it be?! // Some tests. @@ -437,6 +399,16 @@ set_next_key :: (key: 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 { @@ -457,105 +429,57 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { defer input_override = xx Keys.None; return input_override; } - + if OS_was_terminal_resized() return xx Keys.Resize; - // FIXME Old version... - // if input_string.count > 0 { - // defer advance(*input_string, 1); - // return input_string[0]; - // } - // FIXME New version... - if input_string.count > 0 #no_abc { // TODO Some trickery requires no_abc... which is not that nice in this case... - utf8_bytes := count_utf8_bytes(input_string[0]); - - // TODO We're assuming the terminal buffer contains the entirety of what we need to read for the UTF8 symbols. - if utf8_bytes > input_string.count { + 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); + } - diff := utf8_bytes - input_string.count; + if OS_was_terminal_resized() return xx Keys.Resize; - // TODO Test this and make sure it's working...drop the following lines using Ctrl+V to fill the terminal buffer at once: - // d€€€a - // 1234567890123456789012345678901234567890 - print(""); - - // 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]; - } - aaa := OS_read_input(input_buffer.data + input_string.count, utf8_bytes-input_string.count); // TODO Does not check for read errors. - assert(aaa == diff, "READ MORE THAN EXPECTED"); - input_string.data = input_buffer.data; - input_string.count = utf8_bytes; + 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, utf8_bytes); + advance(*input_string, to_parse.count); return key; } - is_input_available := OS_wait_for_input(timeout_milliseconds); - - if OS_was_terminal_resized() return xx Keys.Resize; - - // FIXME Old version... - // if is_input_available { - // bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. - // if bytes_read > 0 { - // input_string.data = input_buffer.data; - // input_string.count = bytes_read; - // defer advance(*input_string, 1); - // return input_string[0]; - // } + // TODO try_parse_escape_code + // { + // assert(false, "TODO try_parse_escape_code"); // } - // FIXME New version... - if is_input_available { - bytes_read := OS_read_input(input_buffer.data, input_buffer.count); // TODO Does not check for read errors. - if bytes_read > 0 { - input_string.data = input_buffer.data; - input_string.count = bytes_read; - utf8_bytes := count_utf8_bytes(input_string[0]); - - assert(utf8_bytes <= input_string.count, "The input buffer is too small."); // TODO Improve error message. - - // TODO This is only being done after the OS_wait_for_input... for now! - 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; - - /// /// /// /// /// /// /// /// /// - // DEBUG - // br, bc := get_cursor_position(); - // column := 3; - // row += 1; - // nr, rc := get_terminal_size(); - // - // if row >= (rc - 5) then row = 5; - // - // set_cursor_position(row, column); - // for 0..input_string.count-1 { - // print("%:% | ", - // FormatInt.{base= 2, minimum_digits = 8, value = input_string[it]}, - // FormatInt.{base= 16, minimum_digits = 2, value = input_string[it]}, - // ); // TODO DEBUG - // } - // set_cursor_position(br, bc); - /// /// /// /// /// /// /// /// /// - } - } return xx Keys.None; } @@ -696,6 +620,7 @@ start :: () { Commands.SetUTF8, Commands.CursorNormalMode, Commands.KeypadNumMode); + OS_prepare_terminal(); initialized = true; @@ -809,12 +734,11 @@ get_terminal_size :: () -> rows: int, columns: int { // Some systems don't allow to query the terminal size directly. // In such cases, measure it indirectly by the maximum possible cursor position. else { - #import "Math"; // TODO Maybe use S16_MAX values directly. flush_input(); cursor_row, cursor_column := get_cursor_position(); defer set_cursor_position(cursor_row, cursor_column); - set_cursor_position(S16_MAX, S16_MAX); + set_cursor_position(0xFFFF, 0xFFFF); rows, columns = get_cursor_position(); } @@ -824,8 +748,7 @@ get_terminal_size :: () -> rows: int, columns: int { set_cursor_position :: (row: int, column: int) { assert_is_initialized(); auto_release_temp(); - tmp_string := tprint(Commands.SetCursorPosition, row, column); - write_string(tmp_string); + print(Commands.SetCursorPosition, row, column); } get_cursor_position :: () -> row: int, column: int { @@ -868,29 +791,6 @@ set_terminal_title :: (title: string) { } -test :: () { - - // A) Testing stuff -#if true { - start(); - flush_input(); - set_cursor_position(S16_MAX, S16_MAX); - r_a, c_a := get_cursor_position(); - stop(); - print("\n\rA) size: %, %\n", r_a, c_a); -} - - // B) Built way -#if true { - start(); - r_b, c_b := get_terminal_size(); - stop(); - print("\n\rB) size: %, %\n", r_b, c_b); -} - -} - - #if OS == .WINDOWS { // Prototyping zone... keep clear! } diff --git a/TUI/unix.jai b/TUI/unix.jai index 2da4437..861fe11 100644 --- a/TUI/unix.jai +++ b/TUI/unix.jai @@ -204,6 +204,7 @@ //////////////////////////////////////////////////////////////////////////////// // Resize detection +was_resized : bool; resize_handler :: (signal_code : s32) #c_call { new_context : Context; diff --git a/TUI/windows.jai b/TUI/windows.jai index f704262..ff4d6a1 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -1,16 +1,23 @@ #scope_file +#import "Basic"; #import "Atomics"; #import "System"; #import "Windows"; // https://learn.microsoft.com/windows/win32/winprog/windows-data-types LPVOID :: *void; - LPDWORD :: *s32; BOOL :: bool; + CHAR :: s8; + WCHAR :: s16; SHORT :: s16; + USHORT :: u16; WORD :: u16; DWORD :: s32; + LPDWORD :: *s32; + + + 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 @@ -18,11 +25,16 @@ 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) -> bool #foreign kernel32; + 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; @@ -33,8 +45,17 @@ // 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-waitforsingleobjectex - WaitForSingleObjectEx :: (hHandle: HANDLE, dwMilliseconds: DWORD, bAlertable: BOOL) -> 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 @@ -42,7 +63,7 @@ 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. - _UNUSED_0008_; + 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_; @@ -86,6 +107,49 @@ dwMaximumWindowSize : COORD; } + 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; + } + //Event : EventUnion; + } + + INPUT_RECORD_EVENT_TYPE :: enum s32 { + KEY_EVENT :: 0x0001; + MOUSE_EVENT :: 0x0002; + WINDOW_BUFFER_SIZE_EVENT :: 0x0004; + MENU_EVENT :: 0x0008; + FOCUS_EVENT :: 0x0010; + } + + KEY_EVENT_RECORD :: struct { + bKeyDown : BOOL; + wRepeatCount : WORD; + 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; + } stdin: HANDLE; initial_stdin_mode: u32; @@ -98,6 +162,7 @@ //////////////////////////////////////////////////////////////////////////////// // Resize detection +was_resized : bool; resize_handler :: (signal_code : s32) #c_call { /* TODO @@ -131,6 +196,36 @@ restore_resize_handler :: () { */ } +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 @@ -187,44 +282,101 @@ OS_reset_terminal :: () { } OS_flush_input :: inline () { - // TODO - https://learn.microsoft.com/en-us/windows/console/flushconsoleinputbuffer - // BOOL WINAPI FlushConsoleInputBuffer(_In_ HANDLE hConsoleInput); /* 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; success := ReadConsoleA(stdin, buffer, cast(s32)bytes_to_read, *bytes_read); if success == false { _, error_message := get_error_value_and_string(); return -1, true, error_message; } + // print(">%:%<", bytes_to_read, bytes_read); TODO DEBUG return bytes_read; } -OS_wait_for_input :: (timeout_milliseconds: s32) -> is_input_available: bool { +// timeout_milliseconds +// 0: do not wait +// -1: wait indefinitely +OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { /* TODO Try to implement using: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobjectex + + This wait procedure on windows should check if next input is valid (keyboard down) before returning, otherwise it should discar that input and go back to wait (if more sleep is allowed). */ - poll_return := WaitForSingleObjectEx(stdin, timeout_milliseconds, true); - error_code, error_message := get_error_value_and_string(); // FIXME Not used. + expiration := current_time_monotonic() + to_apollo(timeout_milliseconds / 1000.0); + // Possible values for poll_return TODO NOT BEING USED WAIT_ABANDONED :: 0x00000080; - WAIT_IO_COMPLETION :: 0x000000C0; + //WAIT_IO_COMPLETION :: 0x000000C0; WAIT_OBJECT_0 :: 0x00000000; WAIT_TIMEOUT :: 0x00000102; WAIT_FAILED :: 0xFFFFFFFF; - - return ifx poll_return == 0 then true else false; + //return ifx poll_return == WAIT_OBJECT_0 then true else false; + + 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); + } +// if poll_return != WAIT_OBJECT_0 then return false; + + // Discard invalid input until a valid input is found or no input is left. + count := count_input(); + while count > 0 { + record := peek_input(); + if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true || record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + return true; + } + read_input(); // TODO Discard input. + count -= 1; + } + + now := current_time_monotonic(); + if now >= expiration then return false; + timeout_milliseconds = xx to_milliseconds(expiration - now); + } + + return false; } 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. + + defer was_resized = false; // TODO Not using this flag... what happens if ... not sure what... :thinking: + + flag: = false; + + record := peek_input(); + while record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + read_input(); + record = peek_input(); + flag = true; + } + + //return was_resized; + return flag; } diff --git a/ttt.jai b/ttt.jai index d4a5320..6759342 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1237,13 +1237,6 @@ main :: () { // -- -- -- Testing TUI -- START - // TODO Test input - - { - TUI.test(); - exit(0); - } - if 1 { print("TEST : set and get cursor position --\n", to_standard_error = true); TUI.start(); @@ -1268,8 +1261,16 @@ main :: () { while(key != #char "q") { __mark := get_temporary_storage_mark(); key = TUI.get_key(1000); - if key >= 32 && key <= 128 then print_character(cast,force(u8)key); - else write_string("-"); + 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)); + } set_temporary_storage_mark(__mark); } TUI.stop(); @@ -1307,7 +1308,7 @@ main :: () { print("> success\n", to_standard_error = true); } - #if 1 { + if 1 { print("TEST : print keys and set terminal title --\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); @@ -1400,6 +1401,8 @@ main :: () { TUI.stop(); } + write_string("DONE"); + exit(0); // -- -- -- Testing TUI -- STOP -- cgit v1.2.3 From b3c9a89d19a3021f56a8d9249307a0c38ee5cfd6 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 10 Feb 2024 16:46:54 +0000 Subject: Improved TUI tests. --- ttt.jai | 62 +++++++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 23 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 6759342..8987a77 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1238,19 +1238,19 @@ main :: () { // -- -- -- Testing TUI -- START if 1 { - print("TEST : set and get cursor position --\n", to_standard_error = true); + print("TEST : set and get cursor position\n", to_standard_error = true); TUI.start(); ROW :: 3; COLUMN :: 3; TUI.set_cursor_position(ROW, COLUMN); row, column := TUI.get_cursor_position(); TUI.stop(); - assert(row == ROW && column == COLUMN, "Failed set/get cursor position.\n"); - print("> success\n", to_standard_error = true); + assert(row == ROW && column == COLUMN, "# Failed set/get cursor position.\n"); + print("- success\n", to_standard_error = true); } if 1 { - print("TEST : test key input --\n", to_standard_error = true); + print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); @@ -1274,11 +1274,11 @@ main :: () { set_temporary_storage_mark(__mark); } TUI.stop(); - print("> success\n", to_standard_error = true); + print("- success\n", to_standard_error = true); } - if 0 { - print("TEST : draw box --\n", to_standard_error = true); + if 1 { + print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); @@ -1287,12 +1287,14 @@ main :: () { print("Can you see the box below? (y/n)"); key := TUI.get_key(); TUI.stop(); - assert(key == #char "y", "Failed to draw box.\n"); - print("> success\n", to_standard_error = true); + + print("\n\n\rDEBUG DEBUG WIP WIP WIP TODO HACK\n\n\r>%<\n\r", TUI.to_string(key)); // TODO WIP Currently debugging this. + assert(key == #char "y", "# Failed to draw box.\n"); + print("- success\n", to_standard_error = true); } - if 0 { - print("TEST : get terminal size --\n", to_standard_error = true); + if 1 { + print("TEST : get terminal size\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); @@ -1304,15 +1306,30 @@ main :: () { key = TUI.get_key(); } TUI.stop(); - assert(key == #char "y", "Failed to get terminal size.\n"); - print("> success\n", to_standard_error = true); + assert(key == #char "y", "# Failed to get terminal size.\n"); + print("- success\n", to_standard_error = true); + } + + if 1 { + print("TEST : set terminal title\n", to_standard_error = true); + TUI.start(); + 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(); + } + TUI.stop(); + assert(key == #char "y", "# Failed to set terminal title.\n"); + print("- success\n", to_standard_error = true); } if 1 { - print("TEST : print keys and set terminal title --\n", to_standard_error = true); + print("TEST : print keys and set terminal title\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); - xcolumns, xrows: int; key: TUI.Key = #char "d"; last_none_char := "X"; drop_down := 0; @@ -1361,17 +1378,16 @@ main :: () { y := ifx size_c > 24 then size_c-24 else 1; TUI.set_cursor_position(x, y); print("size(CxR): %x%\n", size_c, size_r); - - key = TUI.get_key(3000); + + key = TUI.get_key(1000); set_temporary_storage_mark(__mark); } TUI.stop(); - print("size(CxR): %x%\n", xcolumns, xrows); } - if 1 { - print("TEST : user input --\n", to_standard_error = true); + if 0 { + print("TEST : user input\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); @@ -1384,18 +1400,18 @@ main :: () { case TUI.Keys.Escape; { print("Have you pressed Esc? (y/n)"); assert(TUI.get_key() == #char "y", "Failed to read line on Esc."); - print("> success\n", to_standard_error = true); + print("- success\n", to_standard_error = true); } case TUI.Keys.Resize; { print("Have you resized the terminal? (y/n)"); assert(TUI.get_key() == #char "y", "Failed to read line on resize."); - print("> success\n", to_standard_error = true); + print("- success\n", to_standard_error = true); } case; { print("Have you entered '%'? (y/n)", str); assert(TUI.get_key() == #char "y", "Failed to read line."); - print("> success\n", to_standard_error = true); + print("- success\n", to_standard_error = true); } } TUI.stop(); -- cgit v1.2.3 From cbb4b1f06b2395705851430742e23240ece90510 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 16 Feb 2024 02:33:20 +0000 Subject: Base implementation of TUI/windows. --- TUI/module.jai | 10 ++--- TUI/windows.jai | 116 ++++++++++++++++++++++++++++++++++---------------------- ttt.jai | 8 ++-- 3 files changed, 78 insertions(+), 56 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 0ae0398..9c90805 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -157,14 +157,13 @@ set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, 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; +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 { - // Terminal key-codes have 1 to 6 bytes, so we can signal special cases setting the edge-bytes. - None : Key : 0xF0000000_0000000F; - Resize : Key : 0xF0000000_0000001F; + None : Key : #run to_key("#NONE"); + Resize : Key : #run to_key("#RESIZE"); Space : Key : #char " "; Enter : Key : #char "\r"; @@ -399,7 +398,6 @@ set_next_key :: (key: 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 @@ -429,7 +427,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { defer input_override = xx Keys.None; return input_override; } - + if OS_was_terminal_resized() return xx Keys.Resize; should_read_input := false; diff --git a/TUI/windows.jai b/TUI/windows.jai index ff4d6a1..f79a5cf 100644 --- a/TUI/windows.jai +++ b/TUI/windows.jai @@ -15,6 +15,7 @@ WORD :: u16; DWORD :: s32; LPDWORD :: *s32; + UINT :: u32; PINPUT_RECORD :: *INPUT_RECORD; @@ -107,6 +108,14 @@ 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 { @@ -114,23 +123,14 @@ 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; + MenuEvent : MENU_EVENT_RECORD; + FocusEvent : FOCUS_EVENT_RECORD; } - //Event : EventUnion; } - INPUT_RECORD_EVENT_TYPE :: enum s32 { - KEY_EVENT :: 0x0001; - MOUSE_EVENT :: 0x0002; - WINDOW_BUFFER_SIZE_EVENT :: 0x0004; - MENU_EVENT :: 0x0008; - FOCUS_EVENT :: 0x0010; - } - KEY_EVENT_RECORD :: struct { bKeyDown : BOOL; - wRepeatCount : WORD; + wRepeatCount : WORD #align 4; wVirtualKeyCode : WORD; wVirtualScanCode : WORD; union { @@ -151,6 +151,14 @@ 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; @@ -297,13 +305,23 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo assert(bytes_to_read <= 0x7fff_ffff, "The Windows API only allows to read up to s32 bytes from the standard input."); - bytes_read: s32; - success := ReadConsoleA(stdin, buffer, cast(s32)bytes_to_read, *bytes_read); - if success == false { - _, error_message := get_error_value_and_string(); - return -1, true, error_message; + 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; } - // print(">%:%<", bytes_to_read, bytes_read); TODO DEBUG return bytes_read; } @@ -311,23 +329,22 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, error: bo // 0: do not wait // -1: wait indefinitely OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { - /* TODO - Try to implement using: - https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobjectex - This wait procedure on windows should check if next input is valid (keyboard down) before returning, otherwise it should discar that input and go back to wait (if more sleep is allowed). + /* 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; - //WAIT_IO_COMPLETION :: 0x000000C0; - WAIT_OBJECT_0 :: 0x00000000; - WAIT_TIMEOUT :: 0x00000102; - WAIT_FAILED :: 0xFFFFFFFF; - //return ifx poll_return == WAIT_OBJECT_0 then true else false; + 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); @@ -343,19 +360,33 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo _, error_message := get_error_value_and_string(); assert(false, error_message); } -// if poll_return != WAIT_OBJECT_0 then return false; - // Discard invalid input until a valid input is found or no input is left. + // Discard invalid input events. count := count_input(); while count > 0 { record := peek_input(); - if record.EventType == .KEY_EVENT && record.KeyEvent.bKeyDown == true || record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + + 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(); // TODO Discard input. + + 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); @@ -365,18 +396,11 @@ OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: boo } OS_was_terminal_resized :: () -> bool { - - defer was_resized = false; // TODO Not using this flag... what happens if ... not sure what... :thinking: - - flag: = false; - - record := peek_input(); - while record.EventType == .WINDOW_BUFFER_SIZE_EVENT { + while peek_input().EventType == .WINDOW_BUFFER_SIZE_EVENT { + was_resized = true; read_input(); - record = peek_input(); - flag = true; } - //return was_resized; - return flag; + defer was_resized = false; + return was_resized; } diff --git a/ttt.jai b/ttt.jai index 8987a77..072d671 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1280,15 +1280,14 @@ main :: () { if 1 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + TUI.start(); // TODO Should start() call flush_input internally? + 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(); TUI.stop(); - - print("\n\n\rDEBUG DEBUG WIP WIP WIP TODO HACK\n\n\r>%<\n\r", TUI.to_string(key)); // TODO WIP Currently debugging this. assert(key == #char "y", "# Failed to draw box.\n"); print("- success\n", to_standard_error = true); } @@ -1325,7 +1324,8 @@ main :: () { assert(key == #char "y", "# Failed to set terminal title.\n"); print("- success\n", to_standard_error = true); } - + + // TODO Setup this test... check for Meta+FX or Ctrl+FX compatibility between windows and *nix. if 1 { print("TEST : print keys and set terminal title\n", to_standard_error = true); TUI.start(); -- cgit v1.2.3 From 6a28cf6fb30f96d540b3ecbfd53b9e20ef61869d Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 22 Feb 2024 00:42:50 +0000 Subject: Added TODO to solve incoherent function key codes. --- TUI/module.jai | 53 +++++++++++++++++++++++++++++++++++++---------------- ttt.jai | 11 +++++++---- 2 files changed, 44 insertions(+), 20 deletions(-) (limited to 'ttt.jai') diff --git a/TUI/module.jai b/TUI/module.jai index 2813c17..c8fff63 100644 --- a/TUI/module.jai +++ b/TUI/module.jai @@ -14,38 +14,39 @@ #import "String"; #import "Thread"; +// Special Graphics Characters Drawings :: struct { - /* - TODO - Using unicode allows to just-print instead of jumping back and forth between drawing/text modes. - But it seems that not all terminals support unicode?! Do we even bother with those terminals?! - */ - // test_drawing_without_mode :: () { - // top := "┌───┐"; - // btm := "└───┘"; - // print(">%<\n", top); - // print(">%<\n", btm); - // } + 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"; - - Blank :: "\x5F"; - Diamond :: "\x60"; - Checkerboard :: "\x61"; - PlusMinus :: "\x67"; LessThanOrEqual :: "\x79"; GreaterThanOrEqual :: "\x7A"; Pi :: "\x7B"; NotEqual :: "\x7C"; + PoundSign :: "\x7D"; CenteredDot :: "\x7E"; } @@ -225,6 +226,26 @@ Keys :: struct #type_info_none { 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"); diff --git a/ttt.jai b/ttt.jai index 072d671..e9adba3 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1358,17 +1358,20 @@ main :: () { case; { TUI.set_cursor_position(3+drop_down, 2); str := TUI.to_string(key); + array_to_print: [..] string; for 0..str.count-1 { - print("'%'", FormatInt.{value = cast(u8)str[it], base=16}); + tmp := tprint("%", FormatInt.{value = cast(u8)str[it], base=16},, temporary_allocator); + array_add(*array_to_print, tmp); } - write_string(":> "); + string_to_print := join(..array_to_print, separator = " "); + print(": % : ", string_to_print); for 0..str.count-1 { if str[it] == #char "\e" { - str[it] = #char "?"; + str[it] = #char "^"; } } write_string(str); - write_string(" Date: Wed, 28 Feb 2024 00:03:27 +0000 Subject: Moved custom modules to newly supported local modules folder. --- Integer_Saturating_Arithmetic.jai | 416 --------------- TUI/module.jai | 823 ------------------------------ TUI/unix.jai | 286 ----------- TUI/windows.jai | 406 --------------- modules/Integer_Saturating_Arithmetic.jai | 416 +++++++++++++++ modules/TUI/module.jai | 823 ++++++++++++++++++++++++++++++ modules/TUI/unix.jai | 286 +++++++++++ modules/TUI/windows.jai | 406 +++++++++++++++ ttt.jai | 2 +- 9 files changed, 1932 insertions(+), 1932 deletions(-) delete mode 100644 Integer_Saturating_Arithmetic.jai delete mode 100644 TUI/module.jai delete mode 100644 TUI/unix.jai delete mode 100644 TUI/windows.jai create mode 100644 modules/Integer_Saturating_Arithmetic.jai create mode 100644 modules/TUI/module.jai create mode 100644 modules/TUI/unix.jai create mode 100644 modules/TUI/windows.jai (limited to 'ttt.jai') diff --git a/Integer_Saturating_Arithmetic.jai b/Integer_Saturating_Arithmetic.jai deleted file mode 100644 index 74643e0..0000000 --- a/Integer_Saturating_Arithmetic.jai +++ /dev/null @@ -1,416 +0,0 @@ -// 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/TUI/module.jai b/TUI/module.jai deleted file mode 100644 index c8fff63..0000000 --- a/TUI/module.jai +++ /dev/null @@ -1,823 +0,0 @@ -// 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" Where = row and = column. - QueryDeviceAttributes :: "\e[0c"; - QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = 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;;t - // where is the number of rows and of columns. - FORMAT :: "\e[8;;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 - // where is the number of rows and of columns. - FORMAT :: "\e[;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/TUI/unix.jai b/TUI/unix.jai deleted file mode 100644 index 861fe11..0000000 --- a/TUI/unix.jai +++ /dev/null @@ -1,286 +0,0 @@ -#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/TUI/windows.jai b/TUI/windows.jai deleted file mode 100644 index f79a5cf..0000000 --- a/TUI/windows.jai +++ /dev/null @@ -1,406 +0,0 @@ -#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; -} 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" Where = row and = column. + QueryDeviceAttributes :: "\e[0c"; + QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = 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;;t + // where is the number of rows and of columns. + FORMAT :: "\e[8;;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 + // where is the number of rows and of columns. + FORMAT :: "\e[;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; +} diff --git a/ttt.jai b/ttt.jai index e9adba3..7c21a1f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -24,8 +24,8 @@ #import "File"; #import "File_Utilities"; #import "String"; +#import "Integer_Saturating_Arithmetic"; TUI :: #import "TUI"; -#load "Integer_Saturating_Arithmetic.jai"; // TODO List: -- cgit v1.2.3 From ce40906a177708e54ea1067ca157a308a185a164 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 29 Feb 2024 01:08:40 +0000 Subject: WIP : Improving get_key to deal with escape codes. --- modules/TUI/module.jai | 56 ++++++++++++++++++++++++++++++++------------------ ttt.jai | 4 ++-- 2 files changed, 38 insertions(+), 22 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 2f32752..3008d23 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -244,8 +244,6 @@ Keys :: struct #type_info_none { ... */ - WIP HERE - F1 : Key : #run to_key("\eOP"); F2 : Key : #run to_key("\eOQ"); F3 : Key : #run to_key("\eOR"); @@ -477,29 +475,47 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { 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; + if input_string.count == 0 return Keys.None; - // 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!? - } + // Assume we're parsing just a single char. + to_parse := input_string; + to_parse.count = 1; - key := to_key(to_parse); - advance(*input_string, to_parse.count); - return key; + // Try to parse UTF8 character. + if is_utf8_continuation_byte(input_string[0]) { + to_parse.count = count_utf8_bytes(input_string[0]); } - // TODO try_parse_escape_code - // { - // assert(false, "TODO try_parse_escape_code"); - // } + // Try to parse escape code. + if input_string[0] == #char "\e" && input_string.count > 1 { + 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!? + + WIP HERE + // A possible way to solve this is to create a LUT, and then, grow the to_parse.count from 2 to KEY_SIZE and return as soon + // as we ding a match on the LUT. + // If the LUT is too big... maybe use a hash-table. + + if compare(to_parse, "\e[A") == 0 { + advance(*input_string, to_parse.count); + return to_key("#UP"); + } + else if compare(to_parse, "\e[B") == 0 { + advance(*input_string, to_parse.count); + return to_key("#DOWN"); + } + else if compare(to_parse, "\e[C") == 0 { + advance(*input_string, to_parse.count); + return to_key("#RIGHT"); + } + else if compare(to_parse, "\e[D") == 0 { + advance(*input_string, to_parse.count); + return to_key("#LEFT"); + } + } - return xx Keys.None; + advance(*input_string, to_parse.count); + return to_key(to_parse); } // TODO Review me! diff --git a/ttt.jai b/ttt.jai index 7c21a1f..194bd06 100644 --- a/ttt.jai +++ b/ttt.jai @@ -14,8 +14,8 @@ // this program. If not, see . // Compilation commands: -// - release : jai ttt.jai -import_dir . -quiet -x64 -release -// - debug : jai ttt.jai -import_dir . -quiet -x64 +// - release : jai ttt.jai -quiet -x64 -release +// - debug : jai ttt.jai -quiet -x64 #import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. #import "System"; -- cgit v1.2.3 From 45e56e387b713cebd78c3789ed7c234e588fbe48 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 1 Mar 2024 02:08:36 +0000 Subject: WIP : First attempt to use hash table on escape codes. --- modules/TUI/module.jai | 57 +++++++++++++++++++++++++++++++++++++++++++++++++- ttt.jai | 4 ++-- 2 files changed, 58 insertions(+), 3 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 3008d23..ed3f517 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -13,6 +13,7 @@ #import "Basic"; #import "String"; #import "Thread"; +#import "Hash_Table"; // Special Graphics Characters Drawings :: struct { @@ -362,6 +363,52 @@ Keys :: struct #type_info_none { CtrlAltShiftF12 : Key : #run to_key("\e[24;8~"); } +key_map: Table(string, Key); + +setup_key_map :: () { + + table_set(*key_map, "\e[A", to_key("#UP")); + table_set(*key_map, "\e[A", to_key("#UP")); + + table_set(*key_map, "\eOQ", to_key("#F2")); + table_set(*key_map, "\e[12~", to_key("#F2")); + + table_set(*key_map, "\eO2Q", to_key("#SF2")); + table_set(*key_map, "\e[1;2Q", to_key("#SF2")); + table_set(*key_map, "\e[12;2~", to_key("#SF2")); + + table_set(*key_map, "\e\e[12~", to_key("#AF2")); + table_set(*key_map, "\e\e[24~", to_key("#AF12")); + + table_set(*key_map, "\e[12^", to_key("#CF2")); + + // A good example + table_set(*key_map, "\e[21~", to_key("#F10")); + table_set(*key_map, "\e[21;1~", to_key("#MF10")); + table_set(*key_map, "\e[21;2~", to_key("#SF10")); + table_set(*key_map, "\e[21;3~", to_key("#AF10")); + table_set(*key_map, "\e[21;4~", to_key("#SAF10")); + table_set(*key_map, "\e[21;5~", to_key("#CF10")); + table_set(*key_map, "\e[21;6~", to_key("#SCF10")); + table_set(*key_map, "\e[21;7~", to_key("#ACF10")); + table_set(*key_map, "\e[21;8~", to_key("#SACF10")); + + table_set(*key_map, "\e[24~", to_key("#F12")); + table_set(*key_map, "\e[24;2~", to_key("#SF12")); + table_set(*key_map, "\e[24;3~", to_key("#AF12")); + + // TODO Try with: + // - konsole + // - xterm + // - rxvt-unicode + // - kittyterminal + // - termux + + // F2 : Key : #run to_key("\eOQ"); + // F2 : 1b 4f 51 : ^OQ : -> \e[12~ + // Shift+ F2 : 1b 4f 32 51 : ^O2Q : -> \e[12;2~ +} + to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { k: Key; // #if DEBUG { @@ -491,10 +538,16 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { 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!? - WIP HERE + // WIP HERE // A possible way to solve this is to create a LUT, and then, grow the to_parse.count from 2 to KEY_SIZE and return as soon // as we ding a match on the LUT. // If the LUT is too big... maybe use a hash-table. + + key, success := table_find(*key_map, to_parse); + if success { + advance(*input_string, to_parse.count); + return key; + } if compare(to_parse, "\e[A") == 0 { advance(*input_string, to_parse.count); @@ -649,6 +702,8 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { start :: () { if initialized == true return; + setup_key_map(); // TODO This is being called multiple times... please fix me! + input_string.data = input_buffer.data; input_string.count = 0; input_override = xx Keys.None; diff --git a/ttt.jai b/ttt.jai index 194bd06..94e868c 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1367,11 +1367,11 @@ main :: () { print(": % : ", string_to_print); for 0..str.count-1 { if str[it] == #char "\e" { - str[it] = #char "^"; + str[it] = #char "#"; } } write_string(str); - write_string(" : ~DAM"); + write_string(" :"); drop_down += 1; } } -- cgit v1.2.3 From 76eab65ffe14674a7b7fdece67c2a25c2a044dc7 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 2 Mar 2024 03:58:33 +0000 Subject: Setup mappings for several different terminals. --- modules/TUI/module.jai | 663 +++++++++++++++++++++++++++++-------------------- ttt.jai | 8 +- 2 files changed, 400 insertions(+), 271 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index ed3f517..a5f5837 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -163,252 +163,6 @@ 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+ F4 : 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~ - ... - */ - - 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~"); -} - -key_map: Table(string, Key); - -setup_key_map :: () { - - table_set(*key_map, "\e[A", to_key("#UP")); - table_set(*key_map, "\e[A", to_key("#UP")); - - table_set(*key_map, "\eOQ", to_key("#F2")); - table_set(*key_map, "\e[12~", to_key("#F2")); - - table_set(*key_map, "\eO2Q", to_key("#SF2")); - table_set(*key_map, "\e[1;2Q", to_key("#SF2")); - table_set(*key_map, "\e[12;2~", to_key("#SF2")); - - table_set(*key_map, "\e\e[12~", to_key("#AF2")); - table_set(*key_map, "\e\e[24~", to_key("#AF12")); - - table_set(*key_map, "\e[12^", to_key("#CF2")); - - // A good example - table_set(*key_map, "\e[21~", to_key("#F10")); - table_set(*key_map, "\e[21;1~", to_key("#MF10")); - table_set(*key_map, "\e[21;2~", to_key("#SF10")); - table_set(*key_map, "\e[21;3~", to_key("#AF10")); - table_set(*key_map, "\e[21;4~", to_key("#SAF10")); - table_set(*key_map, "\e[21;5~", to_key("#CF10")); - table_set(*key_map, "\e[21;6~", to_key("#SCF10")); - table_set(*key_map, "\e[21;7~", to_key("#ACF10")); - table_set(*key_map, "\e[21;8~", to_key("#SACF10")); - - table_set(*key_map, "\e[24~", to_key("#F12")); - table_set(*key_map, "\e[24;2~", to_key("#SF12")); - table_set(*key_map, "\e[24;3~", to_key("#AF12")); - - // TODO Try with: - // - konsole - // - xterm - // - rxvt-unicode - // - kittyterminal - // - termux - - // F2 : Key : #run to_key("\eOQ"); - // F2 : 1b 4f 51 : ^OQ : -> \e[12~ - // Shift+ F2 : 1b 4f 32 51 : ^O2Q : -> \e[12;2~ -} - to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { k: Key; // #if DEBUG { @@ -432,14 +186,411 @@ to_string :: inline (key: Key) -> string { // TODO FIXME TEMPORARY MEMORY } is_escape_code :: inline (key: Key) -> bool { + /* + TODO Check if LSB is not # but there is a `#`, then it's a escape code... or NONE... or RESIZE :S + Or...we could change the special codes and set the `#` at the end... then we could simply do: + return (key && 0x00FF) ^ # == 0 && (key && 0xFF00) == 0 + */ + result := false; + while key != 0 { key >>= 8; - result |= ((key ^ Keys.Escape) == 0); + result |= ((key ^ #char "#") == 0); } return result; } +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"); +} + +key_map: Table(string, Key); + +setup_key_map :: () { + /* + This table was created/tested using the following terminals: + - konsole + - kitty + - xterm + - linux console + */ + + // Up + table_set(*key_map, "\e[A", to_key("#up")); + table_set(*key_map, "\e[1;1A", to_key("#up+m")); + table_set(*key_map, "\e[1;2A", to_key("#up+s")); + 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+x")); + table_set(*key_map, "\e[1;8A", to_key("#up+X")); + // Up - kitty + table_set(*key_map, "\e[1;9A", to_key("#up+m")); + + // Down + table_set(*key_map, "\e[B", to_key("#down")); + table_set(*key_map, "\e[1;1B", to_key("#down+m")); + table_set(*key_map, "\e[1;2B", to_key("#down+s")); + 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+x")); + table_set(*key_map, "\e[1;8B", to_key("#down+X")); + // Down - kitty + table_set(*key_map, "\e[1;9B", to_key("#down+m")); + + // Right + table_set(*key_map, "\e[C", to_key("#right")); + table_set(*key_map, "\e[1;1C", to_key("#right+m")); + table_set(*key_map, "\e[1;2C", to_key("#right+s")); + 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+x")); + table_set(*key_map, "\e[1;8C", to_key("#right+X")); + // Right - kitty + table_set(*key_map, "\e[1;9C", to_key("#right+m")); + + // Left + table_set(*key_map, "\e[D", to_key("#left")); + table_set(*key_map, "\e[1;1D", to_key("#left+m")); + table_set(*key_map, "\e[1;2D", to_key("#left+s")); + 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+x")); + table_set(*key_map, "\e[1;8D", to_key("#left+X")); + // Left - kitty + table_set(*key_map, "\e[1;9D", to_key("#left+m")); + + // Home + table_set(*key_map, "\e[H", to_key("#home")); + table_set(*key_map, "\e[1~", to_key("#home")); + table_set(*key_map, "\e[1;1H", to_key("#home+m")); + table_set(*key_map, "\e[1;2H", to_key("#home+s")); + 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+x")); + table_set(*key_map, "\e[1;8H", to_key("#home+X")); + // Home - kitty + table_set(*key_map, "\e[1;9H", to_key("#home+m")); + + // End + table_set(*key_map, "\e[F", to_key("#end")); + table_set(*key_map, "\e[4~", to_key("#end")); + table_set(*key_map, "\e[1;1F", to_key("#end+m")); + table_set(*key_map, "\e[1;2F", to_key("#end+s")); + 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+x")); + table_set(*key_map, "\e[1;8F", to_key("#end+X")); + // End - kitty + table_set(*key_map, "\e[1;9F", to_key("#end+m")); + + // Insert + table_set(*key_map, "\e[2~", to_key("#ins")); + table_set(*key_map, "\e[2;1~", to_key("#ins+m")); + table_set(*key_map, "\e[2;2~", to_key("#ins+s")); + 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+x")); + table_set(*key_map, "\e[2;8~", to_key("#ins+X")); + // Insert - kitty + table_set(*key_map, "\e[2;9~", to_key("#ins+m")); + + // Delete + table_set(*key_map, "\e[3~", to_key("#del")); + table_set(*key_map, "\e[3;1~", to_key("#del+m")); + table_set(*key_map, "\e[3;2~", to_key("#del+s")); + 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+x")); + table_set(*key_map, "\e[3;8~", to_key("#del+X")); + // Delete - kitty + table_set(*key_map, "\e[3;9~", to_key("#del+m")); + + // Page Up + table_set(*key_map, "\e[5~", to_key("#pup")); + table_set(*key_map, "\e[5;1~", to_key("#pup+m")); + table_set(*key_map, "\e[5;2~", to_key("#pup+s")); + 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+x")); + table_set(*key_map, "\e[5;8~", to_key("#pup+X")); + // Page Up - kitty + table_set(*key_map, "\e[5;9~", to_key("#pup+m")); + + // Page Down + table_set(*key_map, "\e[6~", to_key("#pdown")); + table_set(*key_map, "\e[6;1~", to_key("#pdown+m")); + table_set(*key_map, "\e[6;2~", to_key("#pdown+s")); + 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+x")); + table_set(*key_map, "\e[6;8~", to_key("#pdown+X")); + // Page Down - kitty + table_set(*key_map, "\e[6;9~", to_key("#pdown+m")); + + // F1 + table_set(*key_map, "\eOP", to_key("#f1")); + table_set(*key_map, "\eO1P", to_key("#f1+m")); + table_set(*key_map, "\eO2P", to_key("#f1+s")); + 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+x")); + table_set(*key_map, "\eO8P", to_key("#f1+X")); + // F1 - xterm + table_set(*key_map, "\e[1;2P", to_key("#f1+s")); + 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+x")); + table_set(*key_map, "\e[1;8P", to_key("#f1+X")); + // F1 - kitty + table_set(*key_map, "\e[1;9P", to_key("#f1+m")); + // F1 - linux console + table_set(*key_map, "\e[[A", to_key("#f1")); + table_set(*key_map, "\e[25~", to_key("#f1+s")); + + // F2 + table_set(*key_map, "\eOQ", to_key("#f2")); + table_set(*key_map, "\eO1Q", to_key("#f2+m")); + table_set(*key_map, "\eO2Q", to_key("#f2+s")); + 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+x")); + table_set(*key_map, "\eO8Q", to_key("#f2+X")); + // F2 - xterm + table_set(*key_map, "\e[1;2Q", to_key("#f2+s")); + 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+x")); + table_set(*key_map, "\e[1;8Q", to_key("#f2+X")); + // F2 - kitty + table_set(*key_map, "\e[1;9Q", to_key("#f2+m")); + // F2 - linux console + table_set(*key_map, "\e[[B", to_key("#f2")); + table_set(*key_map, "\e[26~", to_key("#f2+s")); + + // F3 + table_set(*key_map, "\eOR", to_key("#f3")); + table_set(*key_map, "\eO1R", to_key("#f3+m")); + table_set(*key_map, "\eO2R", to_key("#f3+s")); + 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+x")); + table_set(*key_map, "\eO8R", to_key("#f3+X")); + // F3 - xterm + table_set(*key_map, "\e[1;2R", to_key("#f3+s")); + 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+x")); + table_set(*key_map, "\e[1;8R", to_key("#f3+X")); + // F3 - kitty + table_set(*key_map, "\e[1;9R", to_key("#f3+m")); + // F3 - linux console + table_set(*key_map, "\e[[C", to_key("#f3")); + table_set(*key_map, "\e[28~", to_key("#f3+s")); + + // F4 + table_set(*key_map, "\eOS", to_key("#f4")); + table_set(*key_map, "\eO1S", to_key("#f4+m")); + table_set(*key_map, "\eO2S", to_key("#f4+s")); + 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+x")); + table_set(*key_map, "\eO8S", to_key("#f4+X")); + // F4 - xterm + table_set(*key_map, "\e[1;2S", to_key("#f4+s")); + 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+x")); + table_set(*key_map, "\e[1;8S", to_key("#f4+X")); + // F4 - kitty + table_set(*key_map, "\e[1;9S", to_key("#f4+m")); + // F4 - linux console + table_set(*key_map, "\e[[D", to_key("#f4")); + table_set(*key_map, "\e[29~", to_key("#f4+s")); + + // F5 + table_set(*key_map, "\e[15~", to_key("#f5")); + table_set(*key_map, "\e[15;1~", to_key("#f5+m")); + table_set(*key_map, "\e[15;2~", to_key("#f5+s")); + 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+x")); + table_set(*key_map, "\e[15;8~", to_key("#f5+X")); + // F5 - kitty + table_set(*key_map, "\e[15;9~", to_key("#f5+m")); + // F5 - linux console + table_set(*key_map, "\e[[E", to_key("#f5")); + table_set(*key_map, "\e[31~", to_key("#f5+s")); + + // F6 + table_set(*key_map, "\e[17~", to_key("#f6")); + table_set(*key_map, "\e[17;1~", to_key("#f6+m")); + table_set(*key_map, "\e[17;2~", to_key("#f6+s")); + 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+x")); + table_set(*key_map, "\e[17;8~", to_key("#f6+X")); + // F6 - kitty + table_set(*key_map, "\e[17;9~", to_key("#f6+m")); + // F6 - linux console + table_set(*key_map, "\e[32~", to_key("#f6+s")); + + // F7 + table_set(*key_map, "\e[18~", to_key("#f7")); + table_set(*key_map, "\e[18;1~", to_key("#f7+m")); + table_set(*key_map, "\e[18;2~", to_key("#f7+s")); + 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+x")); + table_set(*key_map, "\e[18;8~", to_key("#f7+X")); + // F7 - kitty + table_set(*key_map, "\e[18;9~", to_key("#f7+m")); + // F7 - linux console + table_set(*key_map, "\e[33~", to_key("#f7+s")); + + // F8 + table_set(*key_map, "\e[19~", to_key("#f8")); + table_set(*key_map, "\e[19;1~", to_key("#f8+m")); + table_set(*key_map, "\e[19;2~", to_key("#f8+s")); + 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+x")); + table_set(*key_map, "\e[19;8~", to_key("#f8+X")); + // F8 - kitty + table_set(*key_map, "\e[19;9~", to_key("#f8+m")); + // F8 - linux console + table_set(*key_map, "\e[34~", to_key("#f8+s")); + + // F9 + table_set(*key_map, "\e[20~", to_key("#f9")); + table_set(*key_map, "\e[20;1~", to_key("#f9+m")); + table_set(*key_map, "\e[20;2~", to_key("#f9+s")); + 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+x")); + table_set(*key_map, "\e[20;8~", to_key("#f9+X")); + // F9 - kitty + table_set(*key_map, "\e[20;9~", to_key("#f9+m")); + + // F10 + table_set(*key_map, "\e[21~", to_key("#f10")); + table_set(*key_map, "\e[21;1~", to_key("#f10+m")); + table_set(*key_map, "\e[21;2~", to_key("#f10+s")); + 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+x")); + table_set(*key_map, "\e[21;8~", to_key("#f10+X")); + // F10 - kitty + table_set(*key_map, "\e[21;9~", to_key("#f10+m")); + + // F11 + table_set(*key_map, "\e[23~", to_key("#f11")); + table_set(*key_map, "\e[23;1~", to_key("#f11+m")); + table_set(*key_map, "\e[23;2~", to_key("#f11+s")); + 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+x")); + table_set(*key_map, "\e[23;8~", to_key("#f11+X")); + // F11 - kitty + table_set(*key_map, "\e[23;9~", to_key("#f11+m")); + + // F12 + table_set(*key_map, "\e[24~", to_key("#f12")); + table_set(*key_map, "\e[24;1~", to_key("#f12+m")); + table_set(*key_map, "\e[24;2~", to_key("#f12+s")); + 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+x")); + table_set(*key_map, "\e[24;8~", to_key("#f12+X")); + // F12 - kitty + table_set(*key_map, "\e[24;9~", to_key("#f12+m")); +} + initialized := false; //input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! @@ -543,28 +694,12 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // as we ding a match on the LUT. // If the LUT is too big... maybe use a hash-table. + // TEMPORARY HACK key, success := table_find(*key_map, to_parse); if success { advance(*input_string, to_parse.count); return key; } - - if compare(to_parse, "\e[A") == 0 { - advance(*input_string, to_parse.count); - return to_key("#UP"); - } - else if compare(to_parse, "\e[B") == 0 { - advance(*input_string, to_parse.count); - return to_key("#DOWN"); - } - else if compare(to_parse, "\e[C") == 0 { - advance(*input_string, to_parse.count); - return to_key("#RIGHT"); - } - else if compare(to_parse, "\e[D") == 0 { - advance(*input_string, to_parse.count); - return to_key("#LEFT"); - } } advance(*input_string, to_parse.count); diff --git a/ttt.jai b/ttt.jai index 94e868c..1a74b37 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1348,12 +1348,6 @@ main :: () { TUI.clear_terminal(); drop_down = 0; } - - case TUI.Keys.MetaF7; { - TUI.set_cursor_position(3+drop_down, 2); - drop_down += 1; - write_string("META F7"); - } case; { TUI.set_cursor_position(3+drop_down, 2); @@ -1420,7 +1414,7 @@ main :: () { TUI.stop(); } - write_string("DONE"); + write_string("DONE\n"); exit(0); // -- -- -- Testing TUI -- STOP -- cgit v1.2.3 From ac92da96856603d450e39fa536f287bcf67c0fd7 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 15 Mar 2024 01:46:01 +0000 Subject: Improved key map. --- modules/TUI/key_map.jai | 501 ++++++++++++++++++++++++++++++++++++++++++++++++ modules/TUI/module.jai | 388 +------------------------------------ ttt.jai | 8 +- 3 files changed, 510 insertions(+), 387 deletions(-) create mode 100644 modules/TUI/key_map.jai (limited to 'ttt.jai') diff --git a/modules/TUI/key_map.jai b/modules/TUI/key_map.jai new file mode 100644 index 0000000..42acabf --- /dev/null +++ b/modules/TUI/key_map.jai @@ -0,0 +1,501 @@ +#import "Hash_Table"; + +key_map: Table(string, Key); + +setup_key_map :: () { + /* + This table was created/tested using the following terminals: + - g: gnome (terminal) + - i: kitty + - k: konsole + - l: linux (console) + - 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 index 11b5247..d93e4ff 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,4 +1,3 @@ -// TODO Move TUI into ./modules/TUI so we can stop calling --import_dir on compile. #if OS == { case .LINUX; #load "unix.jai"; @@ -13,7 +12,7 @@ #import "Basic"; #import "String"; #import "Thread"; -#import "Hash_Table"; +#load "key_map.jai"; // Special Graphics Characters Drawings :: struct { @@ -238,387 +237,6 @@ Keys :: struct #type_info_none { F12 : Key : #run to_key("#f12"); } -key_map: Table(string, Key); - -setup_key_map :: () { - /* - This table was created/tested using the following terminals: - - konsole - - kitty - - xterm - - linux console - - 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 - table_set(*key_map, "\e[A", to_key("#up")); - table_set(*key_map, "\e[1;1A", to_key("#up+m")); - table_set(*key_map, "\e[1;2A", to_key("#up+s")); - 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+x")); - table_set(*key_map, "\e[1;8A", to_key("#up+X")); - // Up - kitty - table_set(*key_map, "\e[1;9A", to_key("#up+m")); - - // Down - table_set(*key_map, "\e[B", to_key("#down")); - table_set(*key_map, "\e[1;1B", to_key("#down+m")); - table_set(*key_map, "\e[1;2B", to_key("#down+s")); - 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+x")); - table_set(*key_map, "\e[1;8B", to_key("#down+X")); - // Down - kitty - table_set(*key_map, "\e[1;9B", to_key("#down+m")); - - // Right - table_set(*key_map, "\e[C", to_key("#right")); - table_set(*key_map, "\e[1;1C", to_key("#right+m")); - table_set(*key_map, "\e[1;2C", to_key("#right+s")); - 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+x")); - table_set(*key_map, "\e[1;8C", to_key("#right+X")); - // Right - kitty - table_set(*key_map, "\e[1;9C", to_key("#right+m")); - - // Left - table_set(*key_map, "\e[D", to_key("#left")); - table_set(*key_map, "\e[1;1D", to_key("#left+m")); - table_set(*key_map, "\e[1;2D", to_key("#left+s")); - 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+x")); - table_set(*key_map, "\e[1;8D", to_key("#left+X")); - // Left - kitty - table_set(*key_map, "\e[1;9D", to_key("#left+m")); - - // Home - table_set(*key_map, "\e[H", to_key("#home")); - table_set(*key_map, "\e[1~", to_key("#home")); - table_set(*key_map, "\e[1;1H", to_key("#home+m")); - table_set(*key_map, "\e[1;2H", to_key("#home+s")); - 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+x")); - table_set(*key_map, "\e[1;8H", to_key("#home+X")); - // Home - kitty - table_set(*key_map, "\e[1;9H", to_key("#home+m")); - - // End - table_set(*key_map, "\e[F", to_key("#end")); - table_set(*key_map, "\e[4~", to_key("#end")); - table_set(*key_map, "\e[1;1F", to_key("#end+m")); - table_set(*key_map, "\e[1;2F", to_key("#end+s")); - 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+x")); - table_set(*key_map, "\e[1;8F", to_key("#end+X")); - // End - kitty - table_set(*key_map, "\e[1;9F", to_key("#end+m")); - - // Insert - table_set(*key_map, "\e[2~", to_key("#ins")); - table_set(*key_map, "\e[2;1~", to_key("#ins+m")); - table_set(*key_map, "\e[2;2~", to_key("#ins+s")); - 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+x")); - table_set(*key_map, "\e[2;8~", to_key("#ins+X")); - // Insert - kitty - table_set(*key_map, "\e[2;9~", to_key("#ins+m")); - - // Delete - table_set(*key_map, "\e[3~", to_key("#del")); - table_set(*key_map, "\e[3;1~", to_key("#del+m")); - table_set(*key_map, "\e[3;2~", to_key("#del+s")); - 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+x")); - table_set(*key_map, "\e[3;8~", to_key("#del+X")); - // Delete - kitty - table_set(*key_map, "\e[3;9~", to_key("#del+m")); - - // Page Up - table_set(*key_map, "\e[5~", to_key("#pup")); - table_set(*key_map, "\e[5;1~", to_key("#pup+m")); - table_set(*key_map, "\e[5;2~", to_key("#pup+s")); - 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+x")); - table_set(*key_map, "\e[5;8~", to_key("#pup+X")); - // Page Up - kitty - table_set(*key_map, "\e[5;9~", to_key("#pup+m")); - - // Page Down - table_set(*key_map, "\e[6~", to_key("#pdown")); - table_set(*key_map, "\e[6;1~", to_key("#pdown+m")); - table_set(*key_map, "\e[6;2~", to_key("#pdown+s")); - 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+x")); - table_set(*key_map, "\e[6;8~", to_key("#pdown+X")); - // Page Down - kitty - table_set(*key_map, "\e[6;9~", to_key("#pdown+m")); - - // F1 - table_set(*key_map, "\eOP", to_key("#f1")); - table_set(*key_map, "\eO1P", to_key("#f1+m")); - table_set(*key_map, "\eO2P", to_key("#f1+s")); - 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+x")); - table_set(*key_map, "\eO8P", to_key("#f1+X")); - // F1 - xterm - table_set(*key_map, "\e[1;2P", to_key("#f1+s")); - 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+x")); - table_set(*key_map, "\e[1;8P", to_key("#f1+X")); - // F1 - kitty - table_set(*key_map, "\e[1;9P", to_key("#f1+m")); - // F1 - linux console - table_set(*key_map, "\e[[A", to_key("#f1")); - table_set(*key_map, "\e[25~", to_key("#f1+s")); - - // F2 - table_set(*key_map, "\eOQ", to_key("#f2")); - table_set(*key_map, "\eO1Q", to_key("#f2+m")); - table_set(*key_map, "\eO2Q", to_key("#f2+s")); - 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+x")); - table_set(*key_map, "\eO8Q", to_key("#f2+X")); - // F2 - xterm - table_set(*key_map, "\e[1;2Q", to_key("#f2+s")); - 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+x")); - table_set(*key_map, "\e[1;8Q", to_key("#f2+X")); - // F2 - kitty - table_set(*key_map, "\e[1;9Q", to_key("#f2+m")); - // F2 - linux console - table_set(*key_map, "\e[[B", to_key("#f2")); - table_set(*key_map, "\e[26~", to_key("#f2+s")); - - // F3 - table_set(*key_map, "\eOR", to_key("#f3")); - table_set(*key_map, "\eO1R", to_key("#f3+m")); - table_set(*key_map, "\eO2R", to_key("#f3+s")); - 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+x")); - table_set(*key_map, "\eO8R", to_key("#f3+X")); - // F3 - xterm - table_set(*key_map, "\e[1;2R", to_key("#f3+s")); - 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+x")); - table_set(*key_map, "\e[1;8R", to_key("#f3+X")); - // F3 - kitty - table_set(*key_map, "\e[1;9R", to_key("#f3+m")); - // F3 - linux console - table_set(*key_map, "\e[[C", to_key("#f3")); - table_set(*key_map, "\e[28~", to_key("#f3+s")); - - // F4 - table_set(*key_map, "\eOS", to_key("#f4")); - table_set(*key_map, "\eO1S", to_key("#f4+m")); - table_set(*key_map, "\eO2S", to_key("#f4+s")); - 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+x")); - table_set(*key_map, "\eO8S", to_key("#f4+X")); - // F4 - xterm - table_set(*key_map, "\e[1;2S", to_key("#f4+s")); - 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+x")); - table_set(*key_map, "\e[1;8S", to_key("#f4+X")); - // F4 - kitty - table_set(*key_map, "\e[1;9S", to_key("#f4+m")); - // F4 - linux console - table_set(*key_map, "\e[[D", to_key("#f4")); - table_set(*key_map, "\e[29~", to_key("#f4+s")); - - // F5 - table_set(*key_map, "\e[15~", to_key("#f5")); - table_set(*key_map, "\e[15;1~", to_key("#f5+m")); - table_set(*key_map, "\e[15;2~", to_key("#f5+s")); - 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+x")); - table_set(*key_map, "\e[15;8~", to_key("#f5+X")); - // F5 - kitty - table_set(*key_map, "\e[15;9~", to_key("#f5+m")); - // F5 - linux console - table_set(*key_map, "\e[[E", to_key("#f5")); - table_set(*key_map, "\e[31~", to_key("#f5+s")); - - // F6 - table_set(*key_map, "\e[17~", to_key("#f6")); - table_set(*key_map, "\e[17;1~", to_key("#f6+m")); - table_set(*key_map, "\e[17;2~", to_key("#f6+s")); - 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+x")); - table_set(*key_map, "\e[17;8~", to_key("#f6+X")); - // F6 - kitty - table_set(*key_map, "\e[17;9~", to_key("#f6+m")); - // F6 - linux console - table_set(*key_map, "\e[32~", to_key("#f6+s")); - - // F7 - table_set(*key_map, "\e[18~", to_key("#f7")); - -TODO FINISH THIS... WIP - - // table_set(*key_map, "\e[18;1~", to_key("#f7+m")); - table_set(*key_map, "\e[18;2~", to_key("#f7+s")); // #f7+^ - table_set(*key_map, "\e[18;3~", to_key("#f7+a")); // #f7+a - table_set(*key_map, "\e[18;4~", to_key("#f7+A")); // #f7+A - table_set(*key_map, "\e[18;5~", to_key("#f7+c")); // #f7+s - table_set(*key_map, "\e[18;6~", to_key("#f7+C")); - table_set(*key_map, "\e[18;7~", to_key("#f7+x")); // x - table_set(*key_map, "\e[18;8~", to_key("#f7+X")); // X - // F7 - kitty - // table_set(*key_map, "\e[18;9~", to_key("#f7+m")); // m - // table_set(*key_map, "\e[18;10~",to_key("#f7+M")); // M - // table_set(*key_map, "\e[18;11~",to_key("#f7+ma")); // ma -> y - // table_set(*key_map, "\e[18;12~",to_key("#f7+mA")); // MA -> Y - // table_set(*key_map, "\e[18;13~",to_key("#f7+mc")); // mc -> z - // table_set(*key_map, "\e[18;14~",to_key("#f7+mC")); // MC -> Z - // table_set(*key_map, "\e[18;15~",to_key("#f7+mx")); // mac-> w - // table_set(*key_map, "\e[18;16~",to_key("#f7+mX")); // MAC-> W - // F7 - linux console - table_set(*key_map, "\e[33~", to_key("#f7+s")); - - // F8 - table_set(*key_map, "\e[19~", to_key("#f8")); - table_set(*key_map, "\e[19;1~", to_key("#f8+m")); - table_set(*key_map, "\e[19;2~", to_key("#f8+s")); - 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+x")); - table_set(*key_map, "\e[19;8~", to_key("#f8+X")); - // F8 - kitty - table_set(*key_map, "\e[19;9~", to_key("#f8+m")); - // F8 - linux console - table_set(*key_map, "\e[34~", to_key("#f8+s")); - - // F9 - table_set(*key_map, "\e[20~", to_key("#f9")); - table_set(*key_map, "\e[20;1~", to_key("#f9+m")); - table_set(*key_map, "\e[20;2~", to_key("#f9+s")); - 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+x")); - table_set(*key_map, "\e[20;8~", to_key("#f9+X")); - // F9 - kitty - table_set(*key_map, "\e[20;9~", to_key("#f9+m")); - - // F10 - table_set(*key_map, "\e[21~", to_key("#f10")); - table_set(*key_map, "\e[21;1~", to_key("#f10+m")); - table_set(*key_map, "\e[21;2~", to_key("#f10+s")); - 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+x")); - table_set(*key_map, "\e[21;8~", to_key("#f10+X")); - // F10 - kitty - table_set(*key_map, "\e[21;9~", to_key("#f10+m")); - - // F11 - table_set(*key_map, "\e[23~", to_key("#f11")); - table_set(*key_map, "\e[23;1~", to_key("#f11+m")); - table_set(*key_map, "\e[23;2~", to_key("#f11+s")); - 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+x")); - table_set(*key_map, "\e[23;8~", to_key("#f11+X")); - // F11 - kitty - table_set(*key_map, "\e[23;9~", to_key("#f11+m")); - - // F12 - table_set(*key_map, "\e[24~", to_key("#f12")); - table_set(*key_map, "\e[24;1~", to_key("#f12+m")); - table_set(*key_map, "\e[24;2~", to_key("#f12+s")); - 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+x")); - table_set(*key_map, "\e[24;8~", to_key("#f12+X")); - // F12 - kitty - table_set(*key_map, "\e[24;9~", to_key("#f12+m")); -} - initialized := false; //input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! @@ -728,6 +346,10 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { advance(*input_string, to_parse.count); return key; } + TODO If failed to parse... lets return a single char... but it's not working! + else { + to_parse.count = 1; + } } advance(*input_string, to_parse.count); diff --git a/ttt.jai b/ttt.jai index 1a74b37..2921ff0 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1249,7 +1249,7 @@ main :: () { print("- success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); TUI.start(); @@ -1277,7 +1277,7 @@ main :: () { print("- success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); TUI.start(); // TODO Should start() call flush_input internally? @@ -1292,7 +1292,7 @@ main :: () { print("- success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : get terminal size\n", to_standard_error = true); auto_release_temp(); TUI.start(); @@ -1309,7 +1309,7 @@ main :: () { print("- success\n", to_standard_error = true); } - if 1 { + if 0 { print("TEST : set terminal title\n", to_standard_error = true); TUI.start(); title := "BAZINGA"; -- cgit v1.2.3 From b47287aaa3cfa384ec683ff58e0d247e8bad32b1 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 21 Mar 2024 00:50:42 +0000 Subject: Fix get_key to support escape codes. --- modules/TUI/key_map.jai | 2 +- modules/TUI/module.jai | 48 +++++++++++++++++------------------------------- ttt.jai | 13 ++++++++++--- 3 files changed, 28 insertions(+), 35 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/key_map.jai b/modules/TUI/key_map.jai index 42acabf..b272253 100644 --- a/modules/TUI/key_map.jai +++ b/modules/TUI/key_map.jai @@ -31,7 +31,7 @@ setup_key_map :: () { */ // Up // g i k l x - // table_set(*key_map, "\e[A", to_key("#up")); // + + + + + + 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")); // + + + + diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index d93e4ff..93e410e 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -126,6 +126,8 @@ Colors8b :: struct { White :: 15; } +// Fix color tables using this: https://devmemo.io/cheatsheets/terminal_escape_code/ + // TODO Maybe rename. set_style_colors :: (foreground: u8, background: u8) { print( @@ -261,15 +263,6 @@ set_next_key :: (key: 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 { @@ -321,38 +314,31 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { if input_string.count == 0 return Keys.None; - // Assume we're parsing just a single char. + // By default, parse a single UTF8 character (1 to 4 bytes). to_parse := input_string; - to_parse.count = 1; - - // Try to parse UTF8 character. - if is_utf8_continuation_byte(input_string[0]) { - to_parse.count = count_utf8_bytes(input_string[0]); - } - + to_parse.count = count_utf8_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 { - 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!? - // WIP HERE - // A possible way to solve this is to create a LUT, and then, grow the to_parse.count from 2 to KEY_SIZE and return as soon - // as we ding a match on the LUT. - // If the LUT is too big... maybe use a hash-table. + // Limit number of chars to parse. + to_parse.count = ifx input_string.count > KEY_SIZE then KEY_SIZE else input_string.count; - // TEMPORARY HACK + // 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 an escape code was found, use it. if success { - advance(*input_string, to_parse.count); return key; } - TODO If failed to parse... lets return a single char... but it's not working! - else { - to_parse.count = 1; - } + // else { ... } If we entered this block and did not succeed, then we'll return a single escape character.â } - - advance(*input_string, to_parse.count); + return to_key(to_parse); } diff --git a/ttt.jai b/ttt.jai index 2921ff0..9b08f6d 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1332,7 +1332,12 @@ main :: () { TUI.set_terminal_title("bazinga"); key: TUI.Key = #char "d"; last_none_char := "X"; + + size_r, size_c := TUI.get_terminal_size(); + TUI.clear_terminal(); + TUI.draw_box(1, 1, size_c, size_r); drop_down := 0; + while(key != #char "q") { __mark := get_temporary_storage_mark(); @@ -1345,7 +1350,9 @@ main :: () { case TUI.Keys.Resize; #through; case #char "c"; { + size_r, size_c = TUI.get_terminal_size(); TUI.clear_terminal(); + TUI.draw_box(1, 1, size_c, size_r); drop_down = 0; } @@ -1369,13 +1376,13 @@ main :: () { drop_down += 1; } } - size_r, size_c := TUI.get_terminal_size(); - TUI.draw_box(1, 1, size_c, size_r); + + x := ifx size_r > 1 then size_r-1 else 1; y := ifx size_c > 24 then size_c-24 else 1; + TUI.set_cursor_position(x, y); print("size(CxR): %x%\n", size_c, size_r); - key = TUI.get_key(1000); set_temporary_storage_mark(__mark); -- cgit v1.2.3 From f1fe4f6bcf27e0843d424f4885b0913e220ade2e Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 24 Mar 2024 00:50:01 +0000 Subject: Improved robustness and code clarity on get_key. --- modules/TUI/module.jai | 8 +++++--- ttt.jai | 7 +++---- 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 93e410e..6aeac68 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -332,11 +332,13 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { key, success = table_find(*key_map, to_parse); } - // If an escape code was found, use it. + if success { - return key; + return key; // Escape code found, return it. + } + else { + to_parse.count = 1; // No escape code found, return a single (escape) character. } - // else { ... } If we entered this block and did not succeed, then we'll return a single escape character.â } return to_key(to_parse); diff --git a/ttt.jai b/ttt.jai index 9b08f6d..593e30f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1324,9 +1324,8 @@ main :: () { assert(key == #char "y", "# Failed to set terminal title.\n"); print("- success\n", to_standard_error = true); } - - // TODO Setup this test... check for Meta+FX or Ctrl+FX compatibility between windows and *nix. - if 1 { + + if 0 { print("TEST : print keys and set terminal title\n", to_standard_error = true); TUI.start(); TUI.set_terminal_title("bazinga"); @@ -1390,7 +1389,7 @@ main :: () { TUI.stop(); } - if 0 { + if 1 { print("TEST : user input\n", to_standard_error = true); auto_release_temp(); TUI.start(); -- cgit v1.2.3 From 921e162a7369a45c5ddaa2355bbe8db0259d6526 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 24 Mar 2024 01:58:40 +0000 Subject: WIP : Added note about bug. --- ttt.jai | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 593e30f..f0a3a9e 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1420,6 +1420,40 @@ main :: () { TUI.stop(); } + // BUG + SECOND TIME CALLING TO read_input_line fails... + + if 1 { + print("TEST : hidden user input\n", to_standard_error = true); + auto_release_temp(); + TUI.start(); + TUI.clear_terminal(); + TUI.set_cursor_position(1, 1); + print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); + TUI.set_cursor_position(2, 1); + str, key := TUI.read_input_line(15, false); + TUI.set_cursor_position(3, 1); + if key == { + case TUI.Keys.Escape; { + print("Have you pressed Esc? (y/n)"); + assert(TUI.get_key() == #char "y", "Failed to read line on Esc."); + print("- success\n", to_standard_error = true); + } + + case TUI.Keys.Resize; { + print("Have you resized the terminal? (y/n)"); + assert(TUI.get_key() == #char "y", "Failed to read line on resize."); + print("- success\n", to_standard_error = true); + } + case; { + print("Have you entered '%'? (y/n)", str); + assert(TUI.get_key() == #char "y", "Failed to read line."); + print("- success\n", to_standard_error = true); + } + } + TUI.stop(); + } + write_string("DONE\n"); exit(0); // -- -- -- Testing TUI -- STOP -- cgit v1.2.3 From 10215fb3b2d5a178b8ab0419d58d30af3abab625 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 29 Mar 2024 01:04:23 +0000 Subject: Fixed cursor control on read_input_line. --- modules/TUI/module.jai | 51 +++++++++++++++++++++++++------------------------- ttt.jai | 10 ++++++---- 2 files changed, 31 insertions(+), 30 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 98afe3d..5fdf3b9 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -389,18 +389,18 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string { // 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 Alloc like a boss... - write_strings(Commands.StartBlinking, Commands.BlinkingUnderlineShape); - - row, col := get_cursor_position(); + str := alloc_string(count_limit); // TODO Alloc like a boss... + str.count = 0; 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 + row, col := get_cursor_position(); + write_strings(Commands.StartBlinking, Commands.BlinkingUnderlineShape); + + key := Keys.None; while true { // TODO How to alloc/release temporary memory here? key = get_key(); @@ -412,29 +412,24 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { break; case Keys.Left; - if idx == 0 continue; - idx -= 1; + if idx > 0 then idx -= 1; + + case Keys.Right; + if idx < str.count then idx += 1; case Keys.Home; idx = 0; - case Keys.Right; - if idx == str.count-1 continue; - idx += 1; - case Keys.End; - idx = str.count-1; - while str[idx] == 0 && idx > 0 { - idx -= 1; - } - idx += 1; + idx = str.count; case Keys.Delete; - if idx == str.count-1 continue; + if idx == str.count continue; for idx..str.count-2 { str.data[it] = str.data[it+1]; } str.data[str.count-1] = 0; + str.count -= 1; case Keys.Backspace; if idx == 0 continue; @@ -443,17 +438,21 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { str.data[it] = str.data[it+1]; } str.data[str.count-1] = 0; + str.count -= 1; case; - if idx >= str.count continue; - if !is_escape_code(key) { - key_str := to_string(key); - for < str.count-1..idx { - str.data[it] = str.data[it-1]; - } - str.data[idx] = key_str.data[0]; - idx += 1; + if idx >= count_limit continue; + if is_escape_code(key) continue; + + for < count_limit..idx+1 { + str.data[it] = str.data[it-1]; } + + key_str := to_string(key); + str.data[idx] = key_str.data[0]; + + if str.count < count_limit then str.count += 1; + idx += 1; } diff --git a/ttt.jai b/ttt.jai index f0a3a9e..6f0f426 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1396,7 +1396,8 @@ main :: () { TUI.clear_terminal(); TUI.set_cursor_position(1, 1); print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); - TUI.set_cursor_position(2, 1); + r, c := TUI.get_cursor_position(); + TUI.set_cursor_position(r+1, 1); str, key := TUI.read_input_line(15); TUI.set_cursor_position(3, 1); if key == { @@ -1421,16 +1422,17 @@ main :: () { } // BUG - SECOND TIME CALLING TO read_input_line fails... + // SECOND TIME CALLING TO read_input_line fails... - if 1 { + if 0 { print("TEST : hidden user input\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); TUI.set_cursor_position(1, 1); print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); - TUI.set_cursor_position(2, 1); + r, c := TUI.get_cursor_position(); + TUI.set_cursor_position(r+1, 1); str, key := TUI.read_input_line(15, false); TUI.set_cursor_position(3, 1); if key == { -- cgit v1.2.3 From bc5b1e651e17e97177af29762ef7d0558f83db89 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 29 Mar 2024 01:18:46 +0000 Subject: Fixed input echo on read_input_line. --- modules/TUI/module.jai | 9 +++------ ttt.jai | 7 ++----- 2 files changed, 5 insertions(+), 11 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 5fdf3b9..9cc7e5d 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -460,17 +460,14 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { // set_cursor_position(row, col); // write_builder(*builder, false); if is_visible { - set_cursor_position(row, col+idx); - for idx..count_limit print_character(#char " "); set_cursor_position(row, col); write_string(str); + for str.count..count_limit-1 print_character(#char " "); } else { set_cursor_position(row, col); - for 0..str.count-1 { - char := cast(u8) ifx str[it] != 0 then #char "*" else #char " "; - print_character(char); - } + for 0..str.count-1 print_character(#char "*"); + for str.count..count_limit-1 print_character(#char " "); } // print(">%<", builder_to_string(*builder,, temporary_allocator)); set_cursor_position(row, col+idx); diff --git a/ttt.jai b/ttt.jai index 6f0f426..7690c68 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1420,17 +1420,14 @@ main :: () { } TUI.stop(); } - - // BUG - // SECOND TIME CALLING TO read_input_line fails... - if 0 { + if 1 { print("TEST : hidden user input\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); TUI.set_cursor_position(1, 1); - print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); + print("Enter some secret (use Enter to finish, Esc to cancel, or resize to abort):"); r, c := TUI.get_cursor_position(); TUI.set_cursor_position(r+1, 1); str, key := TUI.read_input_line(15, false); -- cgit v1.2.3 From 940d025194d4074833e5302945387bb957acc6b8 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 30 Mar 2024 13:17:23 +0000 Subject: Setting up TUI input functions on ttt. --- ttt.jai | 241 +++++++++++++++++++++++++++++++++------------------------------- 1 file changed, 125 insertions(+), 116 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 7690c68..1053ad7 100644 --- a/ttt.jai +++ b/ttt.jai @@ -33,17 +33,6 @@ TUI :: #import "TUI"; stdscr: *void; // TODO DAM A_BOLD: s32 = 0; // TODO DAM COLOR_PAIR :: (a: s32) -> s32 { return 0; } // TODO DAM -KEY_RESIZE :: 410; // TODO DAM -KEY_F2 :: 266; // TODO DAM -KEY_F5 :: 269; // TODO DAM -KEY_HOME :: 262; // TODO DAM -KEY_UP :: 259; // TODO DAM -KEY_DOWN :: 258; // TODO DAM -KEY_NPAGE :: 338; // TODO DAM -KEY_PPAGE :: 339; // TODO DAM -KEY_END :: 360; // TODO DAM -KEY_BACKSPACE :: 263; // TODO DAM -KEY_DC :: 330; // TODO DAM WINDOW :: struct { } // TODO DAM @@ -90,10 +79,10 @@ app_directory : string; db_file_path : string; ar_file_path : string; -size_x : s32; -size_y : s32; -pos_x : s32; -pos_y : s32; +size_x : int; +size_y : int; +pos_x : int; +pos_y : int; Styles :: enum s16 { SELECTED :: 1; @@ -118,8 +107,8 @@ print_error :: (format :string, args : .. Any) { } else { CHAR_SPACE :: #char " "; - w_size_x: s32 = ifx size_x > 120 then 120 else size_x - 2; - w_size_y: s32 = 4; + w_size_x: int = ifx size_x > 120 then 120 else size_x - 2; + w_size_y: int = 4; if (error_window == null) { //error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); TODO DAM //wattron(error_window, COLOR_PAIR(xx Styles.ERROR)); TODO DAM @@ -1175,23 +1164,27 @@ free_memory :: () { //reset_temporary_storage(); } -read_input_string_padded :: (row: int, column: int, style: s32, length: int, padding: int) -> string { - str := talloc_string(length); - memset(str.data, 0, str.count); - // attron(style | A_UNDERLINE); TODO DAM - // mvprintw(xx row, xx column, "%*s", padding, ""); TODO DAM - //echo(); TODO DAM - //curs_set(1); TODO DAM - //mvgetnstr(xx row, xx column, str.data, xx length); TODO DAM - truncate_string(str, length); - //noecho(); TODO DAM - //curs_set(0); TODO DAM - //attrset(A_NORMAL); TODO DAM - return str; -} +read_input_string :: (row: int, column: int, style: s32, input_limit: int, padding: int = 0) -> value: string, success: bool { + + // TODO We still need to apply the style. + + // TODO Draw padding (at end of inputbox)... padding was renamed to input_width... is this the best name? + // TODO Maybe add another optional arg with the placeholder text (to be preset on the input)? + + // TODO WIPWIPWIP + + column += 1; // TODO FIX THE NCURSES INDEXING CHAOS + + TUI.set_cursor_position(row, column + input_limit); + write_string(TUI.Commands.DrawingMode); + for 1..padding { + write_string(TUI.Drawings.Checkerboard); + } + write_string(TUI.Commands.TextMode); + TUI.set_cursor_position(row, column); + value, key := TUI.read_input_line(input_limit); + return value, key == TUI.Keys.Enter; -read_input_string :: (row: int, column: int, style: s32, length: int) -> string { - return read_input_string_padded(row, column, style, length, length); } // Returns success. @@ -1203,41 +1196,67 @@ read_input_int :: (row: int, style: s32, message: string) -> value: int, success //attrset(A_NORMAL); TODO DAM // Get line number. - input_pos_x := 0;//getcurx(stdscr); TODO DAM - input_width := size_x - input_pos_x - 1; + input_pos_x := 1;//getcurx(stdscr); TODO DAM + input_width := size_x - input_pos_x; str := read_input_string(row, input_pos_x, style, input_width); value, success := parse_int(*str); return value, success; } -// Retuns true if user presses enter, false otherwise. -read_input_char :: (row: int, style: int, message: string) -> s32 { - assert(message.data != null, ASSERT_NOT_NULL, "message"); // TODO Improve this check? - - // attron(xx style); TODO DAM - //move(xx row, 1); TODO DAM +// Shows message to user and waits for user key press. +prompt_user :: (row: int, style: int, message: string) -> TUI.Key { +/* + assert(message.data != null, ASSERT_NOT_NULL, "message"); + attron(xx style); + move(xx row, 1); for 0..size_x-3 { - //for (int idx = 0; idx < size_x - 2; idx++) { // TODO check what's going on here. - //addch(ACS_CKBOARD); TODO DAM + // for (int idx = 0; idx < size_x - 2; idx++) { + addch(ACS_CKBOARD); } - //mvaddstr(xx row, 2, message.data); TODO DAM - //attrset(A_NORMAL); TODO DAM + mvaddstr(xx row, 2, message.data); + attrset(A_NORMAL); - //return getch(); TODO DAM - return 0; -} + return getch(); +*/ -// Retuns true if user presses enter, false otherwise. -read_enter_confirmation :: inline (row: int, style: int, message: string) -> bool { - return read_input_char(row, style, message) == #char "\n"; + // TODO We still need to apply the style. + + TUI.set_cursor_position(row, 2); + write_string(TUI.Commands.DrawingMode); + for 0..size_x-2 { + print(TUI.Drawings.Checkerboard); + } + write_string(TUI.Commands.TextMode); + + TUI.set_cursor_position(row, 3); + write_strings(" ", message, " "); + return TUI.get_key(); } main :: () { // -- -- -- Testing TUI -- START - if 1 { + perform_test := false; + + assert_result :: (result: bool, error_message: string) { + if result == true { + print("- success\n", to_standard_error = true); + } + else { + TUI.stop(); + print("- ERROR: %", error_message, to_standard_error = true); + exit(1); + } + } + + next_line :: inline () { + r, c := TUI.get_cursor_position(); + TUI.set_cursor_position(r+1, 1); + } + + if perform_test && 1 { print("TEST : set and get cursor position\n", to_standard_error = true); TUI.start(); ROW :: 3; @@ -1245,21 +1264,19 @@ main :: () { TUI.set_cursor_position(ROW, COLUMN); row, column := TUI.get_cursor_position(); TUI.stop(); - assert(row == ROW && column == COLUMN, "# Failed set/get cursor position.\n"); - print("- success\n", to_standard_error = true); + assert_result(row == ROW && column == COLUMN, "Failed set/get cursor position.\n"); } - if 0 { + if perform_test && 1 { print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); TUI.start(); 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."); - TUI.set_cursor_position(2, 1); + next_line(); key: TUI.Key; while(key != #char "q") { - __mark := get_temporary_storage_mark(); key = TUI.get_key(1000); if key == TUI.Keys.None { write_string("-"); @@ -1271,13 +1288,12 @@ main :: () { // else if key >= 32 && key <= 128 then print_character(cast,force(u8)key) write_string(TUI.to_string(key)); } - set_temporary_storage_mark(__mark); } TUI.stop(); print("- success\n", to_standard_error = true); } - if 0 { + if perform_test && 1 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); TUI.start(); // TODO Should start() call flush_input internally? @@ -1288,11 +1304,10 @@ main :: () { print("Can you see the box below? (y/n)"); key := TUI.get_key(); TUI.stop(); - assert(key == #char "y", "# Failed to draw box.\n"); - print("- success\n", to_standard_error = true); + assert_result(key == #char "y", "Failed to draw box.\n"); } - if 0 { + if perform_test && 1 { print("TEST : get terminal size\n", to_standard_error = true); auto_release_temp(); TUI.start(); @@ -1305,11 +1320,10 @@ main :: () { key = TUI.get_key(); } TUI.stop(); - assert(key == #char "y", "# Failed to get terminal size.\n"); - print("- success\n", to_standard_error = true); + assert_result(key == #char "y", "Failed to get terminal size.\n"); } - if 0 { + if perform_test && 1 { print("TEST : set terminal title\n", to_standard_error = true); TUI.start(); title := "BAZINGA"; @@ -1321,12 +1335,12 @@ main :: () { key = TUI.get_key(); } TUI.stop(); - assert(key == #char "y", "# Failed to set terminal title.\n"); - print("- success\n", to_standard_error = true); + assert_result(key == #char "y", "Failed to set terminal title.\n"); } - if 0 { + if perform_test && 1 { print("TEST : print keys and set terminal title\n", to_standard_error = true); + auto_release_temp(); TUI.start(); TUI.set_terminal_title("bazinga"); key: TUI.Key = #char "d"; @@ -1338,7 +1352,6 @@ main :: () { drop_down := 0; while(key != #char "q") { - __mark := get_temporary_storage_mark(); if key == { case TUI.Keys.None; { @@ -1383,83 +1396,80 @@ main :: () { TUI.set_cursor_position(x, y); print("size(CxR): %x%\n", size_c, size_r); key = TUI.get_key(1000); - - set_temporary_storage_mark(__mark); + + // __mark := get_temporary_storage_mark(); + // set_temporary_storage_mark(__mark); } + print("- success"); TUI.stop(); } - if 1 { + if perform_test && 1 { print("TEST : user input\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); TUI.set_cursor_position(1, 1); print("Enter some text (use Enter to finish, Esc to cancel, or resize to abort):"); - r, c := TUI.get_cursor_position(); - TUI.set_cursor_position(r+1, 1); + next_line(); str, key := TUI.read_input_line(15); TUI.set_cursor_position(3, 1); + error_message: string; if key == { case TUI.Keys.Escape; { print("Have you pressed Esc? (y/n)"); - assert(TUI.get_key() == #char "y", "Failed to read line on Esc."); - print("- success\n", to_standard_error = true); + error_message = "Failed to read line on Esc."; } case TUI.Keys.Resize; { print("Have you resized the terminal? (y/n)"); - assert(TUI.get_key() == #char "y", "Failed to read line on resize."); - print("- success\n", to_standard_error = true); + error_message = "Failed to read line on resize."; + } case; { print("Have you entered '%'? (y/n)", str); - assert(TUI.get_key() == #char "y", "Failed to read line."); - print("- success\n", to_standard_error = true); + error_message = "Failed to read line."; } } + answer := TUI.get_key(); TUI.stop(); + assert_result(answer == #char "y", error_message); } - if 1 { + if perform_test && 1 { print("TEST : hidden user input\n", to_standard_error = true); auto_release_temp(); TUI.start(); TUI.clear_terminal(); TUI.set_cursor_position(1, 1); print("Enter some secret (use Enter to finish, Esc to cancel, or resize to abort):"); - r, c := TUI.get_cursor_position(); - TUI.set_cursor_position(r+1, 1); + next_line(); str, key := TUI.read_input_line(15, false); TUI.set_cursor_position(3, 1); + error_message: string; if key == { case TUI.Keys.Escape; { print("Have you pressed Esc? (y/n)"); - assert(TUI.get_key() == #char "y", "Failed to read line on Esc."); - print("- success\n", to_standard_error = true); + error_message = "Failed to read line on Esc."; } case TUI.Keys.Resize; { print("Have you resized the terminal? (y/n)"); - assert(TUI.get_key() == #char "y", "Failed to read line on resize."); - print("- success\n", to_standard_error = true); + error_message = "Failed to read line on resize."; } case; { print("Have you entered '%'? (y/n)", str); - assert(TUI.get_key() == #char "y", "Failed to read line."); - print("- success\n", to_standard_error = true); + error_message = "Failed to read line."; } } + answer := TUI.get_key(); TUI.stop(); + assert_result(answer == #char "y", error_message); } - write_string("DONE\n"); - exit(0); // -- -- -- Testing TUI -- STOP - // TODO Implement signal handling and see modules/Debug.jai for examples. - defer report_memory_leaks(); // TODO Remove after final debug sessions. defer free_memory(); @@ -1674,9 +1684,9 @@ main :: () { ifx selected_idx == active_idx && selected_idx != -1 then Styles.ACTIVE else Styles.SELECTED_INVERTED); error_style = A_BOLD | COLOR_PAIR(xx Styles.ERROR); - selected_task_row = ifx is_terminal_too_small then 0 + selected_task_row = ifx is_terminal_too_small then 0 else ifx (selected_idx < 0) then 1 - else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS; + else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS + 1; } if key == { @@ -1697,10 +1707,7 @@ main :: () { // When terminal is resized. case TUI.Keys.Resize; TUI.clear_terminal(); - _y, _x := TUI.get_terminal_size(); - // TODO FIX ME - size_y = xx _y; - size_x = xx _x; + size_y, size_x = 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]; @@ -1723,7 +1730,7 @@ main :: () { case #char "n"; #through; case #char "N"; if is_database_full(db) { - read_enter_confirmation(selected_task_row, error_style, " Unable to create task: database is full. "); + prompt_user(selected_task_row, error_style, "Unable to create task: database is full."); continue; } @@ -1743,29 +1750,31 @@ main :: () { //flushinp(); TODO DAM //ungetch(KEY_F(2)); TODO DAM - case KEY_F2; + case TUI.Keys.F2; if (selected_task == null) continue; - + // Change task name. - input := read_input_string_padded(selected_task_row, 1, action_style, Task.name.count, size_x - 2); + // TODO WIPWIPWIP + input := read_input_string(selected_task_row, 1, action_style, Task.name.count, size_x - 2 - Task.name.count); if is_empty_string(input) == false { - replace_chars(input, "\t\x0B\x0C\r", #char " "); + 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(); } - case KEY_BACKSPACE; + case TUI.Keys.Backspace; if (selected_task == null) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) { + if (prompt_user(selected_task_row, action_style, "Press enter to reset task.") == TUI.Keys.Enter) { reset_task_times(db, db.selected_idx); trigger_autosave(); } - case KEY_DC; // Delete + case TUI.Keys.Delete; if (selected_task == null || selected_task == active_task) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) { + if (prompt_user(selected_task_row, action_style, "Press enter to delete task.") == TUI.Keys.Enter) { delete_task(db, db.selected_idx); trigger_autosave(); } @@ -1851,7 +1860,7 @@ main :: () { if selected_task == null continue; if is_database_full(db) { - read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate task: database is full. "); + prompt_user(selected_task_row, error_style, "Unable to duplicate task: database is full."); continue; } @@ -1861,7 +1870,7 @@ main :: () { } trigger_autosave(); - case KEY_F5; + case TUI.Keys.F5; update_total_times(db); trigger_autosave(); @@ -1912,7 +1921,7 @@ main :: () { if (db != *archive || selected_task == null) continue; if is_database_full(*database) { - read_enter_confirmation(selected_task_row, error_style, " Unable to restore task: database is full. "); + prompt_user(selected_task_row, error_style, "Unable to restore task: database is full."); continue; } @@ -1927,7 +1936,7 @@ main :: () { case #char "s"; #through; case #char "S"; // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. - sort_by := read_input_char(selected_task_row, action_style, " Sort by (n) name, (1..7) day, or (t) total time. "); + sort_by := prompt_user(selected_task_row, action_style, "Sort by (n) name, (1..7) day, or (t) total time."); show_processing(); sort_procedure: (a: Task, b: Task) -> s64; @@ -1975,7 +1984,7 @@ main :: () { case #char "w"; #through; case #char "W"; if (db != *database || db.tasks.count <= 0) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to archive duplicates and reset all. ") == false) continue; + if (prompt_user(selected_task_row, action_style, "Press enter to archive duplicates and reset all.") != TUI.Keys.Enter) continue; show_processing(); for db.tasks { @@ -1990,7 +1999,7 @@ main :: () { case #char "c"; #through; case #char "C"; if (db.tasks.count <= 0) continue; - if (read_enter_confirmation(selected_task_row, action_style, " Press enter to coalesce similar tasks. ") == false) continue; + if (prompt_user(selected_task_row, action_style, "Press enter to coalesce similar tasks.") != TUI.Keys.Enter) continue; show_processing(); head_idx := 0; @@ -2009,7 +2018,7 @@ main :: () { } trigger_autosave(); - case KEY_HOME; + case TUI.Keys.Home; select_task(db, 0); case TUI.Keys.Up; @@ -2018,7 +2027,7 @@ main :: () { case TUI.Keys.PgUp; select_task_by_delta(db, -layout_tasks_rows); - case KEY_END; + case TUI.Keys.End; select_task(db, db.tasks.count-1); case TUI.Keys.Down; -- cgit v1.2.3 From ebf43b78a33c5b878b8cf600c3527c7a78dac3d2 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 30 Mar 2024 13:28:56 +0000 Subject: Allow to set name on new task. --- ttt.jai | 57 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 22 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 1053ad7..5229914 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1205,7 +1205,7 @@ read_input_int :: (row: int, style: s32, message: string) -> value: int, success } // Shows message to user and waits for user key press. -prompt_user :: (row: int, style: int, message: string) -> TUI.Key { +prompt_user_key :: (row: int, style: int, message: string) -> TUI.Key { /* assert(message.data != null, ASSERT_NOT_NULL, "message"); attron(xx style); @@ -1691,7 +1691,7 @@ main :: () { if key == { - // When getch() times out. + // When input times out. case TUI.Keys.None; if (is_autosave_enabled && countdown_to_autosave > 0) { countdown_to_autosave -= INPUT_TIMEOUT_MS; @@ -1711,7 +1711,8 @@ main :: () { 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; @@ -1726,11 +1727,12 @@ main :: () { 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) { - prompt_user(selected_task_row, error_style, "Unable to create task: database is full."); + prompt_user_key(selected_task_row, error_style, "Unable to create task: database is full."); continue; } @@ -1747,9 +1749,10 @@ main :: () { trigger_autosave(); // Force rename action. - //flushinp(); TODO DAM - //ungetch(KEY_F(2)); TODO DAM + TUI.flush_input(); + TUI.set_next_key(TUI.Keys.F2); + // Rename task. case TUI.Keys.F2; if (selected_task == null) continue; @@ -1763,10 +1766,11 @@ main :: () { trigger_autosave(); } + // Reset task timers. case TUI.Keys.Backspace; if (selected_task == null) continue; - if (prompt_user(selected_task_row, action_style, "Press enter to reset task.") == TUI.Keys.Enter) { + if (prompt_user_key(selected_task_row, action_style, "Press enter to reset task.") == TUI.Keys.Enter) { reset_task_times(db, db.selected_idx); trigger_autosave(); } @@ -1774,11 +1778,12 @@ main :: () { case TUI.Keys.Delete; if (selected_task == null || selected_task == active_task) continue; - if (prompt_user(selected_task_row, action_style, "Press enter to delete task.") == TUI.Keys.Enter) { + if (prompt_user_key(selected_task_row, action_style, "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; @@ -1837,6 +1842,7 @@ main :: () { trigger_autosave(); + // Move to. case #char "m"; #through; case #char "M"; if selected_task == null continue; @@ -1846,6 +1852,7 @@ main :: () { 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; @@ -1854,13 +1861,14 @@ main :: () { if success == false continue; target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1; select_task(db, target_index); - + + // Delete. case #char "d"; #through; case #char "D"; if selected_task == null continue; if is_database_full(db) { - prompt_user(selected_task_row, error_style, "Unable to duplicate task: database is full."); + prompt_user_key(selected_task_row, error_style, "Unable to duplicate task: database is full."); continue; } @@ -1870,22 +1878,26 @@ main :: () { } 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/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) { @@ -1904,6 +1916,7 @@ main :: () { db = *database; } + // Archive task. case #char "a"; #through; case #char "A"; if (db != *database || selected_task == null || selected_task == active_task) continue; @@ -1914,14 +1927,14 @@ main :: () { } delete_task(db, db.selected_idx); trigger_autosave(); - - // Restore archived task. + + // Restore task. case #char "r"; #through; case #char "R"; if (db != *archive || selected_task == null) continue; if is_database_full(*database) { - prompt_user(selected_task_row, error_style, "Unable to restore task: database is full."); + prompt_user_key(selected_task_row, error_style, "Unable to restore task: database is full."); continue; } @@ -1936,7 +1949,7 @@ main :: () { case #char "s"; #through; case #char "S"; // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. - sort_by := prompt_user(selected_task_row, action_style, "Sort by (n) name, (1..7) day, or (t) total time."); + sort_by := prompt_user_key(selected_task_row, action_style, "Sort by (n) name, (1..7) day, or (t) total time."); show_processing(); sort_procedure: (a: Task, b: Task) -> s64; @@ -1979,12 +1992,12 @@ main :: () { db.active_idx = find_similar_task(db, active_task); } trigger_autosave(); - + // Workspace cleanup. case #char "w"; #through; case #char "W"; if (db != *database || db.tasks.count <= 0) continue; - if (prompt_user(selected_task_row, action_style, "Press enter to archive duplicates and reset all.") != TUI.Keys.Enter) continue; + if (prompt_user_key(selected_task_row, action_style, "Press enter to archive duplicates and reset all.") != TUI.Keys.Enter) continue; show_processing(); for db.tasks { @@ -1994,12 +2007,12 @@ main :: () { reset_task_times(db, it_index); } trigger_autosave(); - + // Coalesce similar tasks. case #char "c"; #through; case #char "C"; if (db.tasks.count <= 0) continue; - if (prompt_user(selected_task_row, action_style, "Press enter to coalesce similar tasks.") != TUI.Keys.Enter) continue; + if (prompt_user_key(selected_task_row, action_style, "Press enter to coalesce similar tasks.") != TUI.Keys.Enter) continue; show_processing(); head_idx := 0; -- cgit v1.2.3 From 3ea02310f77dcf6aac26152a13a7ec848c78077f Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 31 Mar 2024 23:47:35 +0100 Subject: WIP : Setup TUI styles. --- modules/TUI/module.jai | 87 ++++++++++++++++++++++++++++++++++++++------------ ttt.jai | 10 ++++-- 2 files changed, 73 insertions(+), 24 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 796334a..225b13e 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -103,40 +103,77 @@ Commands :: struct { QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. } -clear_style :: () { - write_string(#run sprint(Commands.SetGraphicsRendition, "0")); +Palette_8b :: 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; } -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; +Color_24b :: struct { + r: u8; + g: u8; + b: u8; } // Fix color tables using this: https://devmemo.io/cheatsheets/terminal_escape_code/ + // TODO Maybe rename. -set_style_colors :: (foreground: u8, background: u8) { +set_style_colors :: (foreground: Palette_8b, background: Palette_8b) { print( #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), - foreground, background); + cast(u8)foreground, cast(u8)background); +} + +set_colors_24b :: (foreground_r: Color_24b, background: Color_24b) { + print( + #run sprint(Commands.SetGraphicsRendition, "38;2;%;%;%;48;2;%;%;%"), + foreground.r, foreground.g, foreground.b, + background.r, background.g, background.b); } // set_colors_24b :: (foreground_r: u255) // TODO https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit +Style :: struct { + background: Palette_8b; + foreground: Palette_8b; + bold: bool; + underline: bool; + strike_through: bool; + negative: bool; +} + +test :: () { + start(); + set_cursor_position(2, 2); + set_style_colors(.RED, .BLACK); + print(" RED \n"); + set_style_colors(.MAGENTA, .BLACK); + print(" MAGENTA \n"); + set_style_colors(.TEAL, .BLACK); + print(" TEAL \n"); + sleep_milliseconds(3000); + stop(); +} + + +clear_style :: () { + write_string(#run sprint(Commands.SetGraphicsRendition, "0")); +} + set_style :: (bold: bool, underline: bool = false, strike_through: bool = false, negative: bool = false) { print( #run sprint(Commands.SetGraphicsRendition, "%;%;%;%"), @@ -386,6 +423,7 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string { } } +// TODO Provide an advanced read_input_line function that allows some styling and to set a placeholder text. read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { /* Use the get_key to read user input and show it on screen. @@ -401,6 +439,13 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { str.count = 0; idx := 0; + // placeholder: string = "", + // { + // copy_size := ifx placeholder.count > str.count then str.count else placeholder.count; + // memcpy(str.data, placeholder.data, copy_size); + // idx = copy_size; + // } + // TODO Some of these may be nice to have: // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line diff --git a/ttt.jai b/ttt.jai index 5229914..35a4bc2 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1059,7 +1059,7 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (task == active_task && task == selected_task) { // attron(COLOR_PAIR(xx Styles.ACTIVE_SELECTED) | A_BOLD); TODO DAM - TUI.set_style_colors(TUI.Colors8b.Red, 6); // TODO TESTING COLORS + TUI.set_style_colors(.RED, 6); // TODO TESTING COLORS TUI.set_style(true, true, true, false); } else if (task == selected_task) { @@ -1067,7 +1067,7 @@ draw_tui :: (db: *Database, layout: *Layout) { TUI.set_style_colors(4, 6); // TODO TESTING COLORS } else if (task == active_task) { - TUI.set_style_colors(TUI.Colors8b.Red, 6); // TODO TESTING COLORS + TUI.set_style_colors(.RED, 6); // TODO TESTING COLORS TUI.set_style(false, false, false, true); // TODO TESTING STYLE // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM } @@ -1224,7 +1224,7 @@ prompt_user_key :: (row: int, style: int, message: string) -> TUI.Key { TUI.set_cursor_position(row, 2); write_string(TUI.Commands.DrawingMode); - for 0..size_x-2 { + for 1..size_x-2 { print(TUI.Drawings.Checkerboard); } write_string(TUI.Commands.TextMode); @@ -1236,6 +1236,10 @@ prompt_user_key :: (row: int, style: int, message: string) -> TUI.Key { main :: () { + TUI.test(); + exit(0); + + // -- -- -- Testing TUI -- START perform_test := false; -- cgit v1.2.3 From ddb3e4f4192009a47fa094aaad5e57da9b8c2711 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 6 Apr 2024 01:51:16 +0100 Subject: Added styles. --- modules/TUI/module.jai | 110 ++++++++-------- modules/TUI/palette_24b.jai | 44 +++++++ modules/TUI/palette_4b.jai | 19 +++ modules/TUI/palette_8b.jai | 307 ++++++++++++++++++++++++++++++++++++++++++++ ttt.jai | 167 +++++++++++++----------- 5 files changed, 514 insertions(+), 133 deletions(-) create mode 100644 modules/TUI/palette_24b.jai create mode 100644 modules/TUI/palette_4b.jai create mode 100644 modules/TUI/palette_8b.jai (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 225b13e..21cd88a 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,3 +1,5 @@ +#module_parameters(COLOR_BIT_DEPTH := 24); + #if OS == { case .LINUX; #load "unix.jai"; @@ -103,84 +105,80 @@ Commands :: struct { QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. } -Palette_8b :: 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; +#if COLOR_BIT_DEPTH == 4 { + #load "palette_4b.jai"; + + set_colors :: inline (foreground: Palette, background: Palette) { + print( + #run sprint("% %", Commands.SetGraphicsRendition, Commands.SetGraphicsRendition), + cast(u8)foreground + 30, cast(u8)background + 40); + } } +else #if COLOR_BIT_DEPTH == 8 { + #load "palette_8b.jai"; -Color_24b :: struct { - r: u8; - g: u8; - b: u8; + set_colors :: inline (foreground: Palette, background: Palette) { + print( + #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), + cast(u8)foreground, cast(u8)background); + } } +else { + #load "palette_24b.jai"; -// Fix color tables using this: https://devmemo.io/cheatsheets/terminal_escape_code/ - - -// TODO Maybe rename. -set_style_colors :: (foreground: Palette_8b, background: Palette_8b) { - print( - #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), - cast(u8)foreground, cast(u8)background); -} + Color_24b :: struct { + r: u8; + g: u8; + b: u8; + } -set_colors_24b :: (foreground_r: Color_24b, background: Color_24b) { - print( - #run sprint(Commands.SetGraphicsRendition, "38;2;%;%;%;48;2;%;%;%"), - foreground.r, foreground.g, foreground.b, - background.r, background.g, background.b); + set_colors :: inline (foreground: Color_24b, background: Color_24b) { + print( + #run sprint(Commands.SetGraphicsRendition, "38;2;%;%;%;48;2;%;%;%"), + foreground.r, foreground.g, foreground.b, + background.r, background.g, background.b); + } } -// set_colors_24b :: (foreground_r: u255) // TODO https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit +#add_context tui_style: Style; Style :: struct { - background: Palette_8b; - foreground: Palette_8b; + #if COLOR_BIT_DEPTH == 4 || COLOR_BIT_DEPTH == 8 { + background: Palette = .BLACK; + foreground: Palette = .WHITE; + } else { + background: Color_24b; + foreground: Color_24b; + } bold: bool; underline: bool; strike_through: bool; negative: bool; } -test :: () { - start(); - set_cursor_position(2, 2); - set_style_colors(.RED, .BLACK); - print(" RED \n"); - set_style_colors(.MAGENTA, .BLACK); - print(" MAGENTA \n"); - set_style_colors(.TEAL, .BLACK); - print(" TEAL \n"); - sleep_milliseconds(3000); - stop(); +set_font_style :: inline (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); } +set_style :: (style: Style) { + set_font_style(style.bold, style.underline, style.strike_through, style.negative); + set_colors(style.foreground, style.background); + context.tui_style = style; +} clear_style :: () { write_string(#run sprint(Commands.SetGraphicsRendition, "0")); } -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); +using_style :: (style: Style) #expand { + __style := context.tui_style; + set_style(style); + `defer set_style(__style); } // TODO Maybe make the OS_* procedures as inline?! diff --git a/modules/TUI/palette_24b.jai b/modules/TUI/palette_24b.jai new file mode 100644 index 0000000..7a263a0 --- /dev/null +++ b/modules/TUI/palette_24b.jai @@ -0,0 +1,44 @@ +// https://www.ditig.com/publications/256-colors-cheat-sheet +Palette :: struct { + 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/ttt.jai b/ttt.jai index 35a4bc2..a07502f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -25,7 +25,7 @@ #import "File_Utilities"; #import "String"; #import "Integer_Saturating_Arithmetic"; -TUI :: #import "TUI"; +TUI :: #import "TUI"(COLOR_BIT_DEPTH=8); // TODO List: @@ -84,13 +84,39 @@ size_y : int; pos_x : int; pos_y : int; -Styles :: enum s16 { - SELECTED :: 1; - SELECTED_INVERTED; - ACTIVE; - ACTIVE_SELECTED; - ERROR; -} +style_default := TUI.Style.{ + background = TUI.Palette.BLACK, + foreground = TUI.Palette.WHITE, +}; + +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, +}; + +style_active := TUI.Style.{ + background = TUI.Palette.BLACK, + foreground = TUI.Palette.BLUE, + bold = true, +}; + +style_active_selected := TUI.Style.{ + background = TUI.Palette.NAVY, + foreground = TUI.Palette.WHITE, + bold = true, +}; + +style_error := TUI.Style.{ + background = TUI.Palette.BLACK, + foreground = TUI.Palette.RED, + bold = true, +}; Layouts :: enum u8 { NORMAL; @@ -157,7 +183,7 @@ trigger_autosave :: () { show_processing :: () { TUI.set_cursor_position(1, 1); - // TODO Maybe add some color? + TUI.using_style(style_active); write_strings(TUI.Commands.DrawingMode, TUI.Drawings.Diamond, TUI.Commands.TextMode); } @@ -915,22 +941,7 @@ initialize_tui :: () { } } - // TODO DAM TUI.start(); - // stdscr = initscr(); // Start curses mode. TODO DAM - // cbreak(); // Line buffering disabled; pass on everty thing to me. TODO DAM - // keypad(stdscr, true); // I need those nifty F1..F12. TODO DAM - // curs_set(0); // Set cursor invisible. TODO DAM - // noecho(); // Disable echoing input characters. TODO DAM - - // Initialize pairs of colors. - //start_color(); TODO DAM - //use_default_colors(); // Using default (-1) instead of COLOR_BLACK. TODO DAM - //init_pair(xx Styles.SELECTED, COLOR_BLACK, COLOR_CYAN); TODO DAM - //init_pair(xx Styles.SELECTED_INVERTED, COLOR_CYAN, -1); TODO DAM - //init_pair(xx Styles.ACTIVE, COLOR_BLUE, -1); TODO DAM - //init_pair(xx Styles.ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE); TODO DAM - //init_pair(xx Styles.ERROR, COLOR_RED, -1); TODO DAM } update_layout :: () { @@ -1015,10 +1026,13 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (idx == now_week_day && active_task != null) { - // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM + TUI.set_style(style_active); } else if (idx == now_week_day) { - // attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); TODO DAM + TUI.set_style(style_selected_inverted); + } + else { + TUI.set_style(style_default); } col = *layout.columns[L_DAYS_IDX + idx]; @@ -1026,9 +1040,6 @@ draw_tui :: (db: *Database, layout: *Layout) { TUI.set_cursor_position(y, x + col.alignment_offset); write_string(col.header); x += col.width; - - // Reset theme. - //attrset(A_NORMAL); TODO DAM } // Headers : total @@ -1058,18 +1069,13 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (task == active_task && task == selected_task) { - // attron(COLOR_PAIR(xx Styles.ACTIVE_SELECTED) | A_BOLD); TODO DAM - TUI.set_style_colors(.RED, 6); // TODO TESTING COLORS - TUI.set_style(true, true, true, false); + TUI.set_style(style_active_selected); } else if (task == selected_task) { - // attron(COLOR_PAIR(xx Styles.SELECTED)); TODO DAM - TUI.set_style_colors(4, 6); // TODO TESTING COLORS + TUI.set_style(style_selected); } else if (task == active_task) { - TUI.set_style_colors(.RED, 6); // TODO TESTING COLORS - TUI.set_style(false, false, false, true); // TODO TESTING STYLE - // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM + TUI.set_style(style_active); } // Task title. @@ -1136,19 +1142,19 @@ draw_tui :: (db: *Database, layout: *Layout) { // Apply theme. if (idx == now_week_day && active_task != null) { - // attron(COLOR_PAIR(xx Styles.ACTIVE) | A_BOLD); TODO DAM + TUI.set_style(style_active); } else if (idx == now_week_day) { - // attron(COLOR_PAIR(xx Styles.SELECTED_INVERTED) | A_BOLD); TODO DAM + TUI.set_style(style_selected_inverted); + } + else { + TUI.set_style(style_default); } column_width = layout.columns[L_DAYS_IDX + idx].width; total_time = add(total_time, daily_total); print_time(y, x, daily_total, column_width); x += column_width; - - // Reset theme. - //attrset(A_NORMAL); TODO DAM } x += 1; print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); @@ -1164,9 +1170,7 @@ free_memory :: () { //reset_temporary_storage(); } -read_input_string :: (row: int, column: int, style: s32, input_limit: int, padding: int = 0) -> value: string, success: bool { - - // TODO We still need to apply the style. +read_input_string :: (row: int, column: int, input_limit: int, padding: int = 0) -> value: string, success: bool { // TODO Draw padding (at end of inputbox)... padding was renamed to input_width... is this the best name? // TODO Maybe add another optional arg with the placeholder text (to be preset on the input)? @@ -1188,7 +1192,7 @@ read_input_string :: (row: int, column: int, style: s32, input_limit: int, paddi } // Returns success. -read_input_int :: (row: int, style: s32, message: string) -> value: int, success: bool { +read_input_int :: (row: int, message: string) -> value: int, success: bool { // attron(xx style); TODO DAM //move(xx row, 1); TODO DAM //addch(ACS_CKBOARD); TODO DAM @@ -1198,14 +1202,14 @@ read_input_int :: (row: int, style: s32, message: string) -> value: int, success // Get line number. input_pos_x := 1;//getcurx(stdscr); TODO DAM input_width := size_x - input_pos_x; - str := read_input_string(row, input_pos_x, style, input_width); + str := read_input_string(row, input_pos_x, input_width); value, success := parse_int(*str); return value, success; } // Shows message to user and waits for user key press. -prompt_user_key :: (row: int, style: int, message: string) -> TUI.Key { +prompt_user_key :: (row: int, message: string) -> TUI.Key { /* assert(message.data != null, ASSERT_NOT_NULL, "message"); attron(xx style); @@ -1236,10 +1240,6 @@ prompt_user_key :: (row: int, style: int, message: string) -> TUI.Key { main :: () { - TUI.test(); - exit(0); - - // -- -- -- Testing TUI -- START perform_test := false; @@ -1656,9 +1656,14 @@ main :: () { db := *database; layout := *layouts[Layouts.COMPACT]; + action_style: TUI.Style; + + TUI.flush_input(); TUI.set_next_key(TUI.Keys.Resize); while (true) { + + TUI.set_style(style_default); if (is_terminal_too_small) { INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; @@ -1679,18 +1684,14 @@ main :: () { // TODO WIP Remove `selected_task` and `active_task` and helper functions. selected_task := get_selected_task(db); active_task := get_active_task(db); - action_style: s32; - error_style: s32; selected_task_row: int; { // TODO Recheck this code. using db; - action_style = A_BOLD | COLOR_PAIR(xx - ifx selected_idx == active_idx && selected_idx != -1 then Styles.ACTIVE - else Styles.SELECTED_INVERTED); - error_style = A_BOLD | COLOR_PAIR(xx Styles.ERROR); - selected_task_row = ifx is_terminal_too_small then 0 - else ifx (selected_idx < 0) then 1 - else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS + 1; + 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 1 + else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS + 1; } if key == { @@ -1736,7 +1737,8 @@ main :: () { case #char "n"; #through; case #char "N"; if is_database_full(db) { - prompt_user_key(selected_task_row, error_style, "Unable to create task: database is full."); + TUI.using_style(style_error); + prompt_user_key(selected_task_row, "Unable to create task: database is full."); continue; } @@ -1762,7 +1764,8 @@ main :: () { // Change task name. // TODO WIPWIPWIP - input := read_input_string(selected_task_row, 1, action_style, Task.name.count, size_x - 2 - Task.name.count); + TUI.using_style(action_style); + input := read_input_string(selected_task_row, 1, Task.name.count, size_x - 2 - Task.name.count); if is_empty_string(input) == false { replace_chars(input, "\t\x0B\x0C\r", #char " "); // Replace weird spaces with space. memset(selected_task.name.data, 0, Task.name.count); @@ -1773,16 +1776,18 @@ main :: () { // Reset task timers. case TUI.Keys.Backspace; if (selected_task == null) continue; - - if (prompt_user_key(selected_task_row, action_style, "Press enter to reset task.") == TUI.Keys.Enter) { + + 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; - - if (prompt_user_key(selected_task_row, action_style, "Press enter to delete task.") == TUI.Keys.Enter) { + + 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(); } @@ -1808,7 +1813,8 @@ main :: () { input_pos_x += 1; // Get input string. - input := read_input_string(selected_task_row, input_pos_x, action_style, input_width); // TODO Temp stringzes. + TUI.using_style(action_style); + input := read_input_string(selected_task_row, input_pos_x, input_width); // TODO Temp stringzes. // Abort if input if empty. if is_empty_string(input) continue; @@ -1850,8 +1856,9 @@ main :: () { case #char "m"; #through; case #char "M"; if selected_task == null continue; - - value, success := read_input_int(selected_task_row, action_style, " Move to: "); + + 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(); @@ -1860,8 +1867,9 @@ main :: () { case #char "g"; #through; case #char "G"; if selected_task == null continue; - - value, success := read_input_int(selected_task_row, action_style, " Go to: "); + + 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); @@ -1872,7 +1880,8 @@ main :: () { if selected_task == null continue; if is_database_full(db) { - prompt_user_key(selected_task_row, error_style, "Unable to duplicate task: database is full."); + TUI.using_style(style_error); + prompt_user_key(selected_task_row, "Unable to duplicate task: database is full."); continue; } @@ -1938,7 +1947,8 @@ main :: () { if (db != *archive || selected_task == null) continue; if is_database_full(*database) { - prompt_user_key(selected_task_row, error_style, "Unable to restore task: database is full."); + TUI.using_style(style_error); + prompt_user_key(selected_task_row, "Unable to restore task: database is full."); continue; } @@ -1953,7 +1963,8 @@ main :: () { case #char "s"; #through; case #char "S"; // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. - sort_by := prompt_user_key(selected_task_row, action_style, "Sort by (n) name, (1..7) day, or (t) total time."); + 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; @@ -2001,7 +2012,8 @@ main :: () { case #char "w"; #through; case #char "W"; if (db != *database || db.tasks.count <= 0) continue; - if (prompt_user_key(selected_task_row, action_style, "Press enter to archive duplicates and reset all.") != TUI.Keys.Enter) 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(); for db.tasks { @@ -2016,7 +2028,8 @@ main :: () { case #char "c"; #through; case #char "C"; if (db.tasks.count <= 0) continue; - if (prompt_user_key(selected_task_row, action_style, "Press enter to coalesce similar tasks.") != TUI.Keys.Enter) 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(); head_idx := 0; -- cgit v1.2.3 From c8fb1f06ec6f83fc71c2f68bb2fc337e971e25b9 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 6 Apr 2024 02:15:41 +0100 Subject: Improved user input prompts. --- modules/TUI/module.jai | 8 +++++++- ttt.jai | 45 ++++++++++++++++++--------------------------- 2 files changed, 25 insertions(+), 28 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 21cd88a..4e67db5 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -448,7 +448,13 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line row, col := get_cursor_position(); - write_strings(Commands.StartBlinking, Commands.BlinkingUnderlineShape); + write_strings(Commands.StartBlinking, Commands.BlinkingBarShape); + + // Clear line for input. + for 1..count_limit { + print_character(#char " "); + } + set_cursor_position(row, col); key := Keys.None; while true { diff --git a/ttt.jai b/ttt.jai index a07502f..517723d 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1175,8 +1175,6 @@ read_input_string :: (row: int, column: int, input_limit: int, padding: int = 0) // TODO Draw padding (at end of inputbox)... padding was renamed to input_width... is this the best name? // TODO Maybe add another optional arg with the placeholder text (to be preset on the input)? - // TODO WIPWIPWIP - column += 1; // TODO FIX THE NCURSES INDEXING CHAOS TUI.set_cursor_position(row, column + input_limit); @@ -1193,15 +1191,24 @@ read_input_string :: (row: int, column: int, input_limit: int, padding: int = 0) // Returns success. read_input_int :: (row: int, message: string) -> value: int, success: bool { - // attron(xx style); TODO DAM - //move(xx row, 1); TODO DAM - //addch(ACS_CKBOARD); TODO DAM - //addstr(message.data); // TODO Convert to C type TODO DAM - //attrset(A_NORMAL); TODO DAM + TUI.set_cursor_position(row, 2); + write_string(TUI.Commands.DrawingMode); + for 1..size_x-2 { + print(TUI.Drawings.Checkerboard); + } + write_string(TUI.Commands.TextMode); + + TUI.set_cursor_position(row, 3); + write_strings(" ", message, " "); // Get line number. - input_pos_x := 1;//getcurx(stdscr); TODO DAM - input_width := size_x - input_pos_x; + input_pos_x := 1 + 1 + 1 + message.count + 1; + input_width := size_x - input_pos_x - 1; + + style_input := context.tui_style; + style_input.underline = true; + TUI.using_style(style_input); + str := read_input_string(row, input_pos_x, input_width); value, success := parse_int(*str); @@ -1210,22 +1217,6 @@ read_input_int :: (row: int, message: string) -> value: int, success: bool { // Shows message to user and waits for user key press. prompt_user_key :: (row: int, message: string) -> TUI.Key { -/* - assert(message.data != null, ASSERT_NOT_NULL, "message"); - attron(xx style); - move(xx row, 1); - for 0..size_x-3 { - // for (int idx = 0; idx < size_x - 2; idx++) { - addch(ACS_CKBOARD); - } - mvaddstr(xx row, 2, message.data); - attrset(A_NORMAL); - - return getch(); -*/ - - // TODO We still need to apply the style. - TUI.set_cursor_position(row, 2); write_string(TUI.Commands.DrawingMode); for 1..size_x-2 { @@ -1858,7 +1849,7 @@ main :: () { if selected_task == null continue; TUI.using_style(action_style); - value, success := read_input_int(selected_task_row, " Move to: "); + 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(); @@ -1869,7 +1860,7 @@ main :: () { if selected_task == null continue; TUI.using_style(action_style); - value, success := read_input_int(selected_task_row, " Go to: "); + 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); -- cgit v1.2.3 From 8016c5c04e3d452dab583b2c9c2161a5a950ea65 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 6 Apr 2024 02:31:50 +0100 Subject: Code cleanup. --- modules/TUI/module.jai | 2 +- ttt.jai | 45 +++++++++++++++++++-------------------------- 2 files changed, 20 insertions(+), 27 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 4e67db5..fbf2dd9 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -718,7 +718,7 @@ set_terminal_title :: (title: string) { print(Commands.SetWindowTitle, title); } - +// TODO #if OS == .WINDOWS { // Prototyping zone... keep clear! } diff --git a/ttt.jai b/ttt.jai index 517723d..9fa62d3 100644 --- a/ttt.jai +++ b/ttt.jai @@ -27,15 +27,6 @@ #import "Integer_Saturating_Arithmetic"; TUI :: #import "TUI"(COLOR_BIT_DEPTH=8); - -// TODO List: -// [ ] Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. -stdscr: *void; // TODO DAM -A_BOLD: s32 = 0; // TODO DAM -COLOR_PAIR :: (a: s32) -> s32 { return 0; } // TODO DAM -WINDOW :: struct { } // TODO DAM - - VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). @@ -123,35 +114,36 @@ Layouts :: enum u8 { COMPACT; } -error_window : *WINDOW = null; +// TODO NEXT Implement error window! + error_time_limit := Apollo_Time.{0, 0}; print_error :: (format :string, args : .. Any) { - if stdscr == null { // || isendwin() == true { // Not in ncurses mode? TODO DAM - print(format, ..args); - print("\n"); - } - else { - CHAR_SPACE :: #char " "; - w_size_x: int = ifx size_x > 120 then 120 else size_x - 2; - w_size_y: int = 4; - if (error_window == null) { + // if stdscr == null { // || isendwin() == true { // Not in ncurses mode? TODO DAM + // print(format, ..args); + // print("\n"); + // } + // else { + // CHAR_SPACE :: #char " "; + // w_size_x: int = ifx size_x > 120 then 120 else size_x - 2; + // w_size_y: int = 4; + // if (error_window == null) { //error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); TODO DAM //wattron(error_window, COLOR_PAIR(xx Styles.ERROR)); TODO DAM //wborder(error_window, CHAR_SPACE, CHAR_SPACE, 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); TODO DAM //mvwprintw(error_window, 0, 1, " Error "); TODO DAM //wmove(error_window, 1, 0); TODO DAM - } - else { + // } + // else { //waddch(error_window, CHAR_SPACE); TODO DAM - } + // } //wprintw(error_window, temp_c_string(tprint(format, args))); TODO DAM - error_time_limit = current_time_monotonic() + seconds_to_apollo(5); - } + // error_time_limit = current_time_monotonic() + seconds_to_apollo(5); + // } } draw_error_window :: () { - if (error_window == null) return; + // if (error_window == null) return; // Hide error window after time-limit or if terminal is shrank. w_size_x: s32; @@ -162,7 +154,7 @@ draw_error_window :: () { || size_y - w_size_y < 2 ) { //delwin(error_window); TODO DAM - error_window = null; + // error_window = null; return; } @@ -1673,6 +1665,7 @@ main :: () { //timeout(INPUT_AWAIT_INF); TODO DAM // TODO WIP Remove `selected_task` and `active_task` and helper functions. + // TODO Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. selected_task := get_selected_task(db); active_task := get_active_task(db); selected_task_row: int; -- cgit v1.2.3 From 194d919eb827e6c6df03917b1732f253e5eac0b5 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Apr 2024 02:46:59 +0100 Subject: Implement error window. --- modules/TUI/module.jai | 32 ++++++++++----------- ttt.jai | 76 +++++++++++++++++++++++--------------------------- 2 files changed, 51 insertions(+), 57 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index fbf2dd9..764effe 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -264,7 +264,7 @@ Keys :: struct #type_info_none { F12 : Key : #run to_key("#f12"); } -initialized := false; +active := false; //input_buffer : [64] u8; // TODO FIXME Input buffer is too small!!! input_buffer : [8] u8; // TODO FIXME Input buffer is too small!!! @@ -277,17 +277,17 @@ input_override : Key; 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."); +assert_is_active :: inline () { + assert(active, "TUI is not ready."); } set_next_key :: (key: Key) { - assert_is_initialized(); + assert_is_active(); input_override = key; } get_key :: (timeout_milliseconds: s32 = -1) -> Key { - assert_is_initialized(); + assert_is_active(); // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte is_utf8_continuation_byte :: inline (byte: u8) -> bool { @@ -371,7 +371,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // TODO Review me! read_input :: (count_limit: int = -1, terminators: .. u8) -> string { - assert_is_initialized(); + assert_is_active(); assert(count_limit >= 0 || terminators.count > 0, "Infinite loop detected, aborting."); // TODO Maybe just return!? @@ -533,7 +533,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { } start :: () { - if initialized == true return; + if active == true return; setup_key_map(); // TODO This is being called multiple times... please fix me! @@ -551,12 +551,12 @@ start :: () { OS_prepare_terminal(); - initialized = true; + active = true; } stop :: () { - if initialized == false return; - initialized = false; + if active == false return; + active = false; OS_reset_terminal(); write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); @@ -574,7 +574,7 @@ flush_input :: () { // } draw_box :: (x: int, y: int, width: int, height: int) { - assert_is_initialized(); + assert_is_active(); // TODO Check if using a String_Builder improves performance (measure it)! // TODO Validate input parameters against the terminal size. @@ -620,13 +620,13 @@ draw_box :: (x: int, y: int, width: int, height: int) { // TODO Maybe rename to "clear()" clear_terminal :: inline () { - assert_is_initialized(); + assert_is_active(); write_string(Commands.ClearScreen); } // TODO Maybe rename to "get_size()" get_terminal_size :: () -> rows: int, columns: int { - assert_is_initialized(); + assert_is_active(); auto_release_temp(); @@ -674,13 +674,13 @@ get_terminal_size :: () -> rows: int, columns: int { } set_cursor_position :: (row: int, column: int) { - assert_is_initialized(); + assert_is_active(); auto_release_temp(); print(Commands.SetCursorPosition, row, column); } get_cursor_position :: () -> row: int, column: int { - assert_is_initialized(); + assert_is_active(); auto_release_temp(); @@ -714,7 +714,7 @@ get_cursor_position :: () -> row: int, column: int { } set_terminal_title :: (title: string) { - assert_is_initialized(); + assert_is_active(); print(Commands.SetWindowTitle, title); } diff --git a/ttt.jai b/ttt.jai index 9fa62d3..2bf3d28 100644 --- a/ttt.jai +++ b/ttt.jai @@ -114,59 +114,53 @@ Layouts :: enum u8 { COMPACT; } -// TODO NEXT Implement error window! -error_time_limit := Apollo_Time.{0, 0}; +error_message: string; + +error_time_limit := Apollo_Time.{0, 0}; print_error :: (format :string, args : .. Any) { - // if stdscr == null { // || isendwin() == true { // Not in ncurses mode? TODO DAM - // print(format, ..args); - // print("\n"); - // } - // else { - // CHAR_SPACE :: #char " "; - // w_size_x: int = ifx size_x > 120 then 120 else size_x - 2; - // w_size_y: int = 4; - // if (error_window == null) { - //error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2); TODO DAM - //wattron(error_window, COLOR_PAIR(xx Styles.ERROR)); TODO DAM - //wborder(error_window, CHAR_SPACE, CHAR_SPACE, 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE); TODO DAM - //mvwprintw(error_window, 0, 1, " Error "); TODO DAM - //wmove(error_window, 1, 0); TODO DAM - // } - // else { - //waddch(error_window, CHAR_SPACE); TODO DAM - // } - //wprintw(error_window, temp_c_string(tprint(format, args))); TODO DAM - // error_time_limit = current_time_monotonic() + seconds_to_apollo(5); - // } + + if TUI.active == false { + print(format, ..args, to_standard_error = true); + print("\n"); + return; + } + + if error_message.data != null { + free(error_message.data); + } + error_message = sprint(format, args); + + error_time_limit = current_time_monotonic() + seconds_to_apollo(5); } draw_error_window :: () { - // if (error_window == null) return; + if error_time_limit < current_time_monotonic() return; - // Hide error window after time-limit or if terminal is shrank. - w_size_x: s32; - w_size_y: s32; - //getmaxyx(error_window, *w_size_y, *w_size_x); TODO DAM + // 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 ) { - //delwin(error_window); TODO DAM - // error_window = null; return; } - // Adjust error window position. - pos_x := (size_x - w_size_x) / 2; - pos_y := (size_y - w_size_y) / 2; - //mvwin(error_window, pos_y, pos_x); TODO DAM - - // Avoid being overwritten by main window content. - //refresh(); TODO DAM - //touchwin(error_window); TODO DAM - //wrefresh(error_window); TODO DAM + 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); + TUI.set_cursor_position(pos_y, pos_x + 1); + write_string(" Error "); + TUI.set_cursor_position(pos_y + 1, pos_x + 1); + for 1..w_size_x-2 { + print_character(#char " "); + } + TUI.set_cursor_position(pos_y + 1, pos_x + 1); + write_string(error_message); } trigger_autosave :: () { @@ -1857,8 +1851,8 @@ main :: () { if success == false continue; target_index := clamp(value, 1, MAX_DATABASE_TASKS) - 1; select_task(db, target_index); - - // Delete. + + // Duplicate. case #char "d"; #through; case #char "D"; if selected_task == null continue; -- cgit v1.2.3 From b3a25d8ef20c98f4bcf917f23d546e54cf41cd35 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Apr 2024 02:56:16 +0100 Subject: Fixed missing default style being applied. --- ttt.jai | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 2bf3d28..8c8d31c 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1020,13 +1020,13 @@ draw_tui :: (db: *Database, layout: *Layout) { else { TUI.set_style(style_default); } - col = *layout.columns[L_DAYS_IDX + idx]; //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM TUI.set_cursor_position(y, x + col.alignment_offset); write_string(col.header); x += col.width; } + TUI.set_style(style_default); // Headers : total x += 1; @@ -1142,6 +1142,7 @@ draw_tui :: (db: *Database, layout: *Layout) { 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); } -- cgit v1.2.3 From 7ddda0dac4b75adc30799eb1a36a84ecc1e41a86 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Apr 2024 04:14:51 +0100 Subject: Replaced row/column arguments by x/y which are more familiar. --- modules/TUI/module.jai | 34 +++++++------- ttt.jai | 118 ++++++++++++++++++++++++------------------------- 2 files changed, 76 insertions(+), 76 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 764effe..5b7861e 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -447,14 +447,14 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { // TODO Some of these may be nice to have: // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line - row, col := get_cursor_position(); + x, y := get_cursor_position(); write_strings(Commands.StartBlinking, Commands.BlinkingBarShape); // Clear line for input. for 1..count_limit { print_character(#char " "); } - set_cursor_position(row, col); + set_cursor_position(x, y); key := Keys.None; while true { @@ -513,17 +513,17 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { } if is_visible { - set_cursor_position(row, col); + set_cursor_position(x, y); write_string(str); for str.count..count_limit-1 print_character(#char " "); } else { - set_cursor_position(row, col); + set_cursor_position(x, y); for 0..str.count-1 print_character(#char "*"); for str.count..count_limit-1 print_character(#char " "); } - set_cursor_position(row, col+idx); + set_cursor_position(x+idx, y); } write_strings(Commands.StopBlinking, Commands.DefaultShape); @@ -535,6 +535,8 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { start :: () { if active == true return; + // TODO Should start() call flush_input internally? + setup_key_map(); // TODO This is being called multiple times... please fix me! input_string.data = input_buffer.data; @@ -625,7 +627,7 @@ clear_terminal :: inline () { } // TODO Maybe rename to "get_size()" -get_terminal_size :: () -> rows: int, columns: int { +get_terminal_size :: () -> width: int, height: int { assert_is_active(); auto_release_temp(); @@ -659,27 +661,25 @@ get_terminal_size :: () -> rows: int, columns: int { rows = parse_int(*parts[1]); columns = parse_int(*parts[2]); } - // Some systems don't allow to query the terminal size directly. + // 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. else { - flush_input(); - cursor_row, cursor_column := get_cursor_position(); - defer set_cursor_position(cursor_row, cursor_column); + x, y := get_cursor_position(); + defer set_cursor_position(x, y); set_cursor_position(0xFFFF, 0xFFFF); - rows, columns = get_cursor_position(); + columns, rows = get_cursor_position(); } - return rows, columns; + return columns, rows; } -set_cursor_position :: (row: int, column: int) { +set_cursor_position :: (x: int, y: int) { assert_is_active(); - auto_release_temp(); - print(Commands.SetCursorPosition, row, column); + print(Commands.SetCursorPosition, y, x); } -get_cursor_position :: () -> row: int, column: int { +get_cursor_position :: () -> x: int, y: int { assert_is_active(); auto_release_temp(); @@ -710,7 +710,7 @@ get_cursor_position :: () -> row: int, column: int { parts := split(input, ";",, temporary_allocator); row := parse_int(*parts[0]); column := parse_int(*parts[1]); - return row, column; + return column, row; } set_terminal_title :: (title: string) { diff --git a/ttt.jai b/ttt.jai index 8c8d31c..88f1323 100644 --- a/ttt.jai +++ b/ttt.jai @@ -153,13 +153,13 @@ draw_error_window :: () { TUI.using_style(style_error); TUI.draw_box(pos_x, pos_y, w_size_x, w_size_y); - TUI.set_cursor_position(pos_y, pos_x + 1); + TUI.set_cursor_position(pos_x + 1, pos_y); write_string(" Error "); - TUI.set_cursor_position(pos_y + 1, pos_x + 1); + TUI.set_cursor_position(pos_x + 1, pos_y + 1); for 1..w_size_x-2 { print_character(#char " "); } - TUI.set_cursor_position(pos_y + 1, pos_x + 1); + TUI.set_cursor_position(pos_x + 1, pos_y + 1); write_string(error_message); } @@ -272,7 +272,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { } } - TUI.set_cursor_position(y, x); + TUI.set_cursor_position(x, y); if time < 0 { print_padding(left_padding); @@ -980,13 +980,13 @@ draw_tui :: (db: *Database, layout: *Layout) { for 0..layout.columns.count-2 { column := layout.columns[it]; x += 1 + column.width; - TUI.set_cursor_position(y, x); + TUI.set_cursor_position(x, y); write_string(TUI.Drawings.TeeT); for row: 2..size_y { - TUI.set_cursor_position(row, x); + TUI.set_cursor_position(x, row); write_string(TUI.Drawings.LineV); } - TUI.set_cursor_position(size_y, x); + TUI.set_cursor_position(x, size_y); write_string(TUI.Drawings.TeeB); } write_string(TUI.Commands.TextMode); @@ -1001,7 +1001,7 @@ draw_tui :: (db: *Database, layout: *Layout) { x += 1; col = *layout.columns[L_TITLE_IDX]; //mvaddstr(xx y, xx (x + col.alignment_offset), ifx db == *archive then layout.archive_title.data else col.header.data); TODO DAM - TUI.set_cursor_position(y, x + col.alignment_offset); + TUI.set_cursor_position(x + col.alignment_offset, y); write_string(ifx db == *archive then layout.archive_title else col.header); x += col.width; @@ -1022,7 +1022,7 @@ draw_tui :: (db: *Database, layout: *Layout) { } col = *layout.columns[L_DAYS_IDX + idx]; //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM - TUI.set_cursor_position(y, x + col.alignment_offset); + TUI.set_cursor_position(x + col.alignment_offset, y); write_string(col.header); x += col.width; } @@ -1032,7 +1032,7 @@ draw_tui :: (db: *Database, layout: *Layout) { x += 1; col = *layout.columns[L_TOTAL_IDX]; //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM - TUI.set_cursor_position(y, x + col.alignment_offset); + TUI.set_cursor_position(x + col.alignment_offset, y); write_string(col.header); @@ -1069,14 +1069,14 @@ draw_tui :: (db: *Database, layout: *Layout) { column_width = layout.columns[L_TITLE_IDX].width; // mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. TODO DAM // TODO FIXME OH MY GOD SUCH BAD CODEZ - TUI.set_cursor_position(y, x); + TUI.set_cursor_position(x, y); white_spaces := column_width; // FIXME Improve by using buffer instead of printing one char at the time. while white_spaces > 0 { print_character(#char " "); white_spaces -= 1; } - TUI.set_cursor_position(y, x); + TUI.set_cursor_position(x, y); task_name: string = cast(string)task.name; task_name.count = ifx task_name.count > column_width then column_width else task_name.count; print("%", task_name); @@ -1107,7 +1107,7 @@ draw_tui :: (db: *Database, layout: *Layout) { /////////////////////////////////////////////////////////////////////////// // 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(size_y, 2); + TUI.set_cursor_position(2, size_y); if (size <= layout.columns[L_TITLE_IDX].width) { print(" %/% ", db.selected_idx + 1, db.tasks.count); } @@ -1157,61 +1157,64 @@ free_memory :: () { //reset_temporary_storage(); } -read_input_string :: (row: int, column: int, input_limit: int, padding: int = 0) -> value: string, success: bool { +read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> value: string, success: bool { // TODO Draw padding (at end of inputbox)... padding was renamed to input_width... is this the best name? // TODO Maybe add another optional arg with the placeholder text (to be preset on the input)? - column += 1; // TODO FIX THE NCURSES INDEXING CHAOS - - TUI.set_cursor_position(row, column + input_limit); + TUI.set_cursor_position(x + input_limit, y); write_string(TUI.Commands.DrawingMode); for 1..padding { write_string(TUI.Drawings.Checkerboard); } write_string(TUI.Commands.TextMode); - TUI.set_cursor_position(row, column); + TUI.set_cursor_position(x, y); value, key := TUI.read_input_line(input_limit); return value, key == TUI.Keys.Enter; } // Returns success. -read_input_int :: (row: int, message: string) -> value: int, success: bool { - TUI.set_cursor_position(row, 2); +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 1..size_x-2 { + for 2..x { print(TUI.Drawings.Checkerboard); } write_string(TUI.Commands.TextMode); - TUI.set_cursor_position(row, 3); + TUI.set_cursor_position(x, y); write_strings(" ", message, " "); - - // Get line number. - input_pos_x := 1 + 1 + 1 + message.count + 1; - input_width := size_x - input_pos_x - 1; + input_pos_x := x + message.count + 2; + input_width := size_x - input_pos_x - 1; + style_input := context.tui_style; style_input.underline = true; TUI.using_style(style_input); - str := read_input_string(row, input_pos_x, input_width); + str := read_input_string(input_pos_x, y, input_width); value, success := parse_int(*str); return value, success; } // Shows message to user and waits for user key press. -prompt_user_key :: (row: int, message: string) -> TUI.Key { - TUI.set_cursor_position(row, 2); +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(row, 3); + TUI.set_cursor_position(x, y); write_strings(" ", message, " "); return TUI.get_key(); } @@ -1234,19 +1237,19 @@ main :: () { } next_line :: inline () { - r, c := TUI.get_cursor_position(); - TUI.set_cursor_position(r+1, 1); + x, y := TUI.get_cursor_position(); + TUI.set_cursor_position(1, y+1); } if perform_test && 1 { print("TEST : set and get cursor position\n", to_standard_error = true); TUI.start(); - ROW :: 3; - COLUMN :: 3; - TUI.set_cursor_position(ROW, COLUMN); - row, column := TUI.get_cursor_position(); + X :: 2; + Y :: 3; + TUI.set_cursor_position(X, Y); + x, y := TUI.get_cursor_position(); TUI.stop(); - assert_result(row == ROW && column == COLUMN, "Failed set/get cursor position.\n"); + assert_result(x == X && y == Y, "Failed set/get cursor position.\n"); } if perform_test && 1 { @@ -1278,7 +1281,7 @@ main :: () { if perform_test && 1 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); - TUI.start(); // TODO Should start() call flush_input internally? + TUI.start(); TUI.flush_input(); TUI.clear_terminal(); TUI.draw_box(1, 2, 5, 3); @@ -1294,9 +1297,9 @@ main :: () { auto_release_temp(); TUI.start(); TUI.clear_terminal(); - rows, columns := TUI.get_terminal_size(); + width, height := TUI.get_terminal_size(); TUI.set_cursor_position(1, 1); - print("Is terminal size % columns and % rows? (y/n)", columns, rows); + 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(); @@ -1328,9 +1331,9 @@ main :: () { key: TUI.Key = #char "d"; last_none_char := "X"; - size_r, size_c := TUI.get_terminal_size(); + width, height := TUI.get_terminal_size(); TUI.clear_terminal(); - TUI.draw_box(1, 1, size_c, size_r); + TUI.draw_box(1, 1, width, height); drop_down := 0; while(key != #char "q") { @@ -1344,14 +1347,14 @@ main :: () { case TUI.Keys.Resize; #through; case #char "c"; { - size_r, size_c = TUI.get_terminal_size(); + width, height = TUI.get_terminal_size(); TUI.clear_terminal(); - TUI.draw_box(1, 1, size_c, size_r); + TUI.draw_box(1, 1, width, height); drop_down = 0; } case; { - TUI.set_cursor_position(3+drop_down, 2); + TUI.set_cursor_position(2, 3+drop_down); str := TUI.to_string(key); array_to_print: [..] string; for 0..str.count-1 { @@ -1371,12 +1374,11 @@ main :: () { } } - - x := ifx size_r > 1 then size_r-1 else 1; - y := ifx size_c > 24 then size_c-24 else 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(CxR): %x%\n", size_c, size_r); + print("size = %x%\n", width, height); key = TUI.get_key(1000); // __mark := get_temporary_storage_mark(); @@ -1395,7 +1397,7 @@ main :: () { 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(3, 1); + TUI.set_cursor_position(1, 3); error_message: string; if key == { case TUI.Keys.Escape; { @@ -1427,7 +1429,7 @@ main :: () { 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(3, 1); + TUI.set_cursor_position(1, 3); error_message: string; if key == { case TUI.Keys.Escape; { @@ -1645,7 +1647,7 @@ main :: () { if (is_terminal_too_small) { INVALID_WINDOW_MESSAGE :: "Terminal is too small: minimum 60x3."; - TUI.set_cursor_position(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE.count) / 2); + TUI.set_cursor_position((size_x - INVALID_WINDOW_MESSAGE.count) / 2, size_y / 2); write_strings(INVALID_WINDOW_MESSAGE); } else { @@ -1691,7 +1693,7 @@ main :: () { // When terminal is resized. case TUI.Keys.Resize; TUI.clear_terminal(); - size_y, size_x = TUI.get_terminal_size(); + 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]; @@ -1742,9 +1744,8 @@ main :: () { if (selected_task == null) continue; // Change task name. - // TODO WIPWIPWIP TUI.using_style(action_style); - input := read_input_string(selected_task_row, 1, Task.name.count, size_x - 2 - Task.name.count); + input := read_input_string(2, selected_task_row, Task.name.count, size_x - 2 - Task.name.count); if is_empty_string(input) == false { replace_chars(input, "\t\x0B\x0C\r", #char " "); // Replace weird spaces with space. memset(selected_task.name.data, 0, Task.name.count); @@ -1784,16 +1785,15 @@ main :: () { // Prepare position to input time operation. selected_day := cast(int)(key - #char "1"); // TODO DAM this cast... input_width := layout.columns[L_DAYS_IDX + selected_day].width; - input_pos_x := 1 + layout.columns[L_TITLE_IDX].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(selected_task_row, input_pos_x, input_width); // TODO Temp stringzes. + input := read_input_string(input_pos_x, selected_task_row, input_width); // TODO Temp stringzes. // Abort if input if empty. if is_empty_string(input) continue; -- cgit v1.2.3 From 6aed607156f476937bc18a80540e2cb2e8c212ec Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Apr 2024 04:26:46 +0100 Subject: Add/remove TODO entries. --- modules/TUI/module.jai | 4 +++- ttt.jai | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 5b7861e..5f143dd 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -421,7 +421,6 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string { } } -// TODO Provide an advanced read_input_line function that allows some styling and to set a placeholder text. read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { /* Use the get_key to read user input and show it on screen. @@ -498,6 +497,9 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { str.count -= 1; case; + + // TODO FIXME Does not support UTF8 input... + if idx >= count_limit continue; if is_escape_code(key) continue; diff --git a/ttt.jai b/ttt.jai index 88f1323..364d161 100644 --- a/ttt.jai +++ b/ttt.jai @@ -966,7 +966,6 @@ draw_tui :: (db: *Database, layout: *Layout) { now_week_day := to_calendar(now_utc, .LOCAL).day_of_week_starting_at_0; // Reset theme and clear screen. - //attrset(A_NORMAL); TODO DAM TUI.clear_terminal(); // Draw outer border. @@ -1000,7 +999,6 @@ draw_tui :: (db: *Database, layout: *Layout) { // Headers : title x += 1; col = *layout.columns[L_TITLE_IDX]; - //mvaddstr(xx y, xx (x + col.alignment_offset), ifx db == *archive then layout.archive_title.data else col.header.data); TODO DAM TUI.set_cursor_position(x + col.alignment_offset, y); write_string(ifx db == *archive then layout.archive_title else col.header); x += col.width; @@ -1021,7 +1019,6 @@ draw_tui :: (db: *Database, layout: *Layout) { TUI.set_style(style_default); } col = *layout.columns[L_DAYS_IDX + idx]; - //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM TUI.set_cursor_position(x + col.alignment_offset, y); write_string(col.header); x += col.width; @@ -1031,7 +1028,6 @@ draw_tui :: (db: *Database, layout: *Layout) { // Headers : total x += 1; col = *layout.columns[L_TOTAL_IDX]; - //mvaddstr(xx y, xx (x + col.alignment_offset), col.header.data); TODO DAM TUI.set_cursor_position(x + col.alignment_offset, y); write_string(col.header); @@ -1099,7 +1095,6 @@ draw_tui :: (db: *Database, layout: *Layout) { print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); // Reset theme. - //attrset(A_NORMAL); TODO DAM TUI.clear_style(); } -- cgit v1.2.3 From a3cf506defb4a01759db6f920a363e23de63e984 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Apr 2024 04:35:43 +0100 Subject: Fixed input width. --- modules/TUI/module.jai | 2 +- ttt.jai | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 5f143dd..bda326b 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,4 +1,4 @@ -#module_parameters(COLOR_BIT_DEPTH := 24); +#module_parameters(COLOR_BIT_DEPTH := 24); // TODO - Do #assert module parameters to make sure they are valid. #if OS == { case .LINUX; diff --git a/ttt.jai b/ttt.jai index 364d161..414ab8c 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1164,6 +1164,11 @@ read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> val } 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; @@ -1185,11 +1190,7 @@ read_input_int :: (y: int, message: string) -> value: int, success: bool { write_strings(" ", message, " "); input_pos_x := x + message.count + 2; - input_width := size_x - input_pos_x - 1; - - style_input := context.tui_style; - style_input.underline = true; - TUI.using_style(style_input); + input_width := size_x - input_pos_x; str := read_input_string(input_pos_x, y, input_width); -- cgit v1.2.3 From 353d8b1145db12ffc42e4f6c2148a848ef8bba84 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 7 Apr 2024 23:23:06 +0100 Subject: Preparing for UTF8 support on read_input_line. --- modules/TUI/module.jai | 18 ++----------- modules/UTF8.jai | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ ttt.jai | 63 +------------------------------------------- 3 files changed, 74 insertions(+), 78 deletions(-) create mode 100644 modules/UTF8.jai (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index bda326b..e7dc21a 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -14,6 +14,7 @@ #import "Basic"; #import "String"; #import "Thread"; +#import "UTF8"; #load "key_map.jai"; // Special Graphics Characters @@ -289,21 +290,6 @@ set_next_key :: (key: Key) { get_key :: (timeout_milliseconds: s32 = -1) -> Key { assert_is_active(); - // 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; @@ -421,7 +407,7 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string { } } -read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { +read_input_line :: (count_limit: int, is_visible: bool = true, placeholder: string = "") -> string, Key { /* 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. diff --git a/modules/UTF8.jai b/modules/UTF8.jai new file mode 100644 index 0000000..fac1326 --- /dev/null +++ b/modules/UTF8.jai @@ -0,0 +1,71 @@ +// 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; +} + +// Truncates the string to the length provided or shorter, in case of UTF8 strings that require so. +// Truncation is done by zeroing the tail of the string in place. +// Returns length of truncated string. +truncate_string :: (str: string, length: int) -> length: int { + if str.data == null then return -1; + + if str.count < length then length = str.count; + + data := str.data; + count := str.count; + + // Find index of first continuation byte. + idx := length; + // while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { TODO REMOVE AFTER TESTING + while (idx > 0 && is_utf8_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, ence '+ 1'. + } + + memset(data + length, 0, count - length); + return length; +} + +// Returns true when the string is empty or consists of space characters. +is_empty_string :: (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; +} diff --git a/ttt.jai b/ttt.jai index 414ab8c..43a972f 100644 --- a/ttt.jai +++ b/ttt.jai @@ -25,6 +25,7 @@ #import "File_Utilities"; #import "String"; #import "Integer_Saturating_Arithmetic"; +#import "UTF8"; TUI :: #import "TUI"(COLOR_BIT_DEPTH=8); VERSION :: "2.0"; // Use only 3 chars (to fit layouts). @@ -189,68 +190,6 @@ count_digits :: (number: s64, base: s64 = 10) -> s64 { return digits; } -Text_Encoding :: enum u8 #specified { - ASCII :: 1; - UTF8 :: 2; -} - -// Truncates the string to the length provided or shorter, in case of UTF8 strings that require so. -// Truncation is done by zeroing the tail of the string in place. -// Returns length of truncated string. -truncate_string :: (str: string, length: s64, $encoding: Text_Encoding = .UTF8) -> length: s64 { - assert(str.data != null, ASSERT_NOT_NULL, "str"); - assert(str.count >= length, "'str.count' should be equal or greater to 'length'."); - - data := str.data; - count := str.count; - - #if encoding == .UTF8 { - // Find index of first continuation byte. - idx := length; - while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { - 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, ence '+ 1'. - } - } - - memset(data + length, 0, count - length); - return length; -} - -// Returns true when the string is empty or consists of space characters. -is_empty_string :: (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; -} - // 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 { -- cgit v1.2.3 From 3491fff478a7ebf7e9281595230528726828f48c Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 9 Apr 2024 15:13:26 +0100 Subject: Clear style when stoping TUI. --- modules/TUI/module.jai | 3 ++- ttt.jai | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index e7dc21a..e0bd9b4 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -547,7 +547,8 @@ start :: () { stop :: () { if active == false return; active = false; - + + clear_style(); OS_reset_terminal(); write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } diff --git a/ttt.jai b/ttt.jai index 43a972f..7fdd9af 100644 --- a/ttt.jai +++ b/ttt.jai @@ -26,7 +26,7 @@ #import "String"; #import "Integer_Saturating_Arithmetic"; #import "UTF8"; -TUI :: #import "TUI"(COLOR_BIT_DEPTH=8); +TUI :: #import "TUI"(COLOR_BIT_DEPTH=4); VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; @@ -99,7 +99,7 @@ style_active := TUI.Style.{ }; style_active_selected := TUI.Style.{ - background = TUI.Palette.NAVY, + background = TUI.Palette.BLUE, foreground = TUI.Palette.WHITE, bold = true, }; -- cgit v1.2.3 From 52c375a02e663a87140ef34f140de16c71f7af5f Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 11 Apr 2024 02:46:03 +0100 Subject: Added UTF8 support to read_input_line. --- modules/TUI/module.jai | 117 +++++++++++++++++++++---------------------------- modules/UTF8.jai | 48 +++++++++++++++++++- ttt.jai | 2 +- 3 files changed, 97 insertions(+), 70 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index e0bd9b4..2d23bff 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,4 +1,4 @@ -#module_parameters(COLOR_BIT_DEPTH := 24); // TODO - Do #assert module parameters to make sure they are valid. +#module_parameters(COLOR_BIT_DEPTH := 24); // TODO - Do #assert module parameters to make sure they are valid... and maybe rename to: COLOR_MODE with a enum. #if OS == { case .LINUX; @@ -111,7 +111,7 @@ Commands :: struct { set_colors :: inline (foreground: Palette, background: Palette) { print( - #run sprint("% %", Commands.SetGraphicsRendition, Commands.SetGraphicsRendition), + #run sprint("%0%0", Commands.SetGraphicsRendition, Commands.SetGraphicsRendition), cast(u8)foreground + 30, cast(u8)background + 40); } } @@ -407,13 +407,13 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string { } } -read_input_line :: (count_limit: int, is_visible: bool = true, placeholder: string = "") -> string, Key { +read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { /* - 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 Escape key. - Resize should discard the input returning an empty string and a Resize key. + 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. */ assert(count_limit >= 0, "Invalid value on count_limit parameter."); @@ -421,32 +421,34 @@ read_input_line :: (count_limit: int, is_visible: bool = true, placeholder: stri str := alloc_string(count_limit); str.count = 0; idx := 0; - - // placeholder: string = "", - // { - // copy_size := ifx placeholder.count > str.count then str.count else placeholder.count; - // memcpy(str.data, placeholder.data, copy_size); - // idx = copy_size; - // } - + // TODO Some of these may be nice to have: // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line x, y := get_cursor_position(); write_strings(Commands.StartBlinking, Commands.BlinkingBarShape); - // Clear line for input. - for 1..count_limit { - print_character(#char " "); - } - set_cursor_position(x, y); - key := Keys.None; while true { auto_release_temp(); - - key = get_key(); + chars_count := count_characters(str); + + // Preview input. + if is_visible { + set_cursor_position(x, y); + write_string(str); + for chars_count..count_limit-1 print_character(#char " "); + } + else { + set_cursor_position(x, y); + for 0..chars_count print_character(#char "*"); + for chars_count..count_limit-1 print_character(#char " "); + } + set_cursor_position(x+idx, y); + + // Process input key. + key = get_key(); if key == { case Keys.Resize; #through; case Keys.Escape; #through; @@ -455,63 +457,47 @@ read_input_line :: (count_limit: int, is_visible: bool = true, placeholder: stri case Keys.Left; if idx > 0 then idx -= 1; - + case Keys.Right; - if idx < str.count then idx += 1; - + if idx < chars_count then idx += 1; + case Keys.Home; idx = 0; case Keys.End; - idx = str.count; + idx = chars_count; case Keys.Delete; - if idx == str.count continue; - for idx..str.count-2 { - str.data[it] = str.data[it+1]; - } - str.data[str.count-1] = 0; - str.count -= 1; - + if idx == chars_count continue; + delete_character(*str, idx); + case Keys.Backspace; if idx == 0 continue; idx -= 1; - for idx..str.count-2 { - str.data[it] = str.data[it+1]; - } - str.data[str.count-1] = 0; - str.count -= 1; - + delete_character(*str, idx); + case; - - // TODO FIXME Does not support UTF8 input... - - if idx >= count_limit continue; if is_escape_code(key) continue; + + buff_idx := map_character_to_buffer_idx(str, idx); + key_str := to_string(key); - for < count_limit..idx+1 { - str.data[it] = str.data[it-1]; + // 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..buff_idx+key_str.count { + str.data[it] = str.data[it-key_str.count]; } - key_str := to_string(key); - str.data[idx] = key_str.data[0]; + memcpy(*str.data[buff_idx], key_str.data, key_str.count); - if str.count < count_limit then str.count += 1; + 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_string(str, count_limit); } - - if is_visible { - set_cursor_position(x, y); - write_string(str); - for str.count..count_limit-1 print_character(#char " "); - } - else { - set_cursor_position(x, y); - for 0..str.count-1 print_character(#char "*"); - for str.count..count_limit-1 print_character(#char " "); - } - - set_cursor_position(x+idx, y); } write_strings(Commands.StopBlinking, Commands.DefaultShape); @@ -559,11 +545,6 @@ flush_input :: () { 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_active(); diff --git a/modules/UTF8.jai b/modules/UTF8.jai index fac1326..eba4585 100644 --- a/modules/UTF8.jai +++ b/modules/UTF8.jai @@ -1,4 +1,5 @@ // BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte +// TODO Maybe rename to: is_continuation_byte is_utf8_continuation_byte :: inline (byte: u8) -> bool { return (byte & 0xC0) == 0x80; } @@ -6,6 +7,7 @@ is_utf8_continuation_byte :: inline (byte: u8) -> bool { // 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 +// TODO Maybe rename to: count_character_bytes count_utf8_bytes :: inline (byte: u8) -> int { if (byte & 0xE0) == 0xC0 return 1+1; if (byte & 0xF0) == 0xE0 return 1+2; @@ -16,6 +18,7 @@ count_utf8_bytes :: inline (byte: u8) -> int { // Truncates the string to the length provided or shorter, in case of UTF8 strings that require so. // Truncation is done by zeroing the tail of the string in place. // Returns length of truncated string. +// TODO Maybe rename to: truncate truncate_string :: (str: string, length: int) -> length: int { if str.data == null then return -1; @@ -26,7 +29,6 @@ truncate_string :: (str: string, length: int) -> length: int { // Find index of first continuation byte. idx := length; - // while (idx > 0 && ((data[idx - 1] & 0xC0) == 0x80)) { TODO REMOVE AFTER TESTING while (idx > 0 && is_utf8_continuation_byte(data[idx - 1])) { idx -= 1; } @@ -48,10 +50,12 @@ truncate_string :: (str: string, length: int) -> length: int { } memset(data + length, 0, count - length); + // str.count = length; TODO We should be doing this... return length; } // Returns true when the string is empty or consists of space characters. +// TODO Maybe rename to: is_empty is_empty_string :: (str: string) -> bool { for 0..str.count-1 { if str[it] == { @@ -69,3 +73,45 @@ is_empty_string :: (str: string) -> bool { } return true; } + +// Counts number of characters in string. +count_characters :: (str: string) -> int { + characters := 0; + idx := 0; + while idx < str.count { + idx += count_utf8_bytes(str[idx]); + characters += 1; + } + return characters; +} + +// Delete character. +delete_character :: (str: *string, character_idx: int) { + buffer_idx := map_character_to_buffer_idx(str.*, character_idx); + bytes_to_delete := count_utf8_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; +} + +// Get character index. +// TODO Maybe rename to: map_character_to_byte_idx or get_character_byte_idx +map_character_to_buffer_idx :: (str: string, character_idx: int) -> buffer_idx: int, success: bool { + if character_idx < 0 then return -1, false; + if character_idx > str.count then return -2, false; + if character_idx == 0 then return 0, true; + + buff_idx := 0; + char_idx := 0; + while buff_idx < str.count && char_idx != character_idx { + buff_idx += count_utf8_bytes(str[buff_idx]); + char_idx += 1; + } + return buff_idx, char_idx == character_idx; +} diff --git a/ttt.jai b/ttt.jai index 7fdd9af..ffe9494 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1674,7 +1674,7 @@ main :: () { TUI.flush_input(); TUI.set_next_key(TUI.Keys.F2); - // Rename task. + // Rename. case TUI.Keys.F2; if (selected_task == null) continue; -- cgit v1.2.3 From 5844ab303d3d7fc396beb7c6d8a0104496608f08 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 14 Apr 2024 18:08:15 +0100 Subject: Add validation to COLOR_MODE module parameter. --- modules/TUI/module.jai | 17 +++++++++-------- ttt.jai | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 7c9d7b1..3843cd7 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,4 +1,4 @@ -#module_parameters(COLOR_BIT_DEPTH := 24); // TODO - Do #assert module parameters to make sure they are valid... and maybe rename to: COLOR_MODE with a enum. +#module_parameters(COLOR_MODE := 24); #if OS == { case .LINUX; @@ -67,10 +67,10 @@ Commands :: struct { SetWindowTitle :: "\e]0;%\e\\"; - RefreshWindow :: "\e[7t"; // TODO Not yet tested. - - SetIEC2022 :: "\e%@"; - SetUTF8 :: "\e%G"; + RefreshWindow :: "\e[7t"; // TODO Not yet tested. Is this required? + + SetIEC2022 :: "\e%@"; // TODO Remove this!? + SetUTF8 :: "\e%G"; // TODO Remove this!? SetGraphicsRendition :: "\e[%m"; @@ -106,7 +106,7 @@ Commands :: struct { QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. } -#if COLOR_BIT_DEPTH == 4 { +#if COLOR_MODE == 4 { #load "palette_4b.jai"; set_colors :: inline (foreground: Palette, background: Palette) { @@ -115,7 +115,7 @@ Commands :: struct { cast(u8)foreground + 30, cast(u8)background + 40); } } -else #if COLOR_BIT_DEPTH == 8 { +else #if COLOR_MODE == 8 { #load "palette_8b.jai"; set_colors :: inline (foreground: Palette, background: Palette) { @@ -144,7 +144,7 @@ else { #add_context tui_style: Style; Style :: struct { - #if COLOR_BIT_DEPTH == 4 || COLOR_BIT_DEPTH == 8 { + #if COLOR_MODE == 4 || COLOR_MODE == 8 { background: Palette = .BLACK; foreground: Palette = .WHITE; } else { @@ -272,6 +272,7 @@ input_string : string; input_override : Key; #run { + assert(COLOR_MODE == 4 || COLOR_MODE == 8 || COLOR_MODE == 24, "Invalid COLOR_MODE. Valid values are 4, 8, or 24 (default)."); assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire Key, which should be able to hold an UTF8 code (4 bytes) or a terminal escape code (6 bytes)."); } diff --git a/ttt.jai b/ttt.jai index ffe9494..13c9fcf 100644 --- a/ttt.jai +++ b/ttt.jai @@ -26,7 +26,7 @@ #import "String"; #import "Integer_Saturating_Arithmetic"; #import "UTF8"; -TUI :: #import "TUI"(COLOR_BIT_DEPTH=4); +TUI :: #import "TUI"(COLOR_MODE=4); VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; -- cgit v1.2.3 From 3112131c24ede1ea343383ffd4f8ca7bc4783f47 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 15 Apr 2024 12:38:18 +0100 Subject: Add module logger that switches between main/alternate screen buffers to write logs. --- modules/TUI/module.jai | 27 ++++++++++++++++++++++----- modules/TUI/windows.jai | 44 +++++++++++++------------------------------- ttt.jai | 27 ++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 37 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 3843cd7..070161c 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -17,6 +17,11 @@ #import "UTF8"; #load "key_map.jai"; +#run { + assert(COLOR_MODE == 4 || COLOR_MODE == 8 || COLOR_MODE == 24, "Invalid COLOR_MODE. Valid values are 4, 8, or 24 (default)."); + assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire Key, which should be able to hold an UTF8 code (4 bytes) or a terminal escape code (6 bytes)."); +} + // Special Graphics Characters Drawings :: struct { Blank :: "\x5F"; @@ -271,9 +276,15 @@ input_buffer : [1024] u8; input_string : string; input_override : Key; -#run { - assert(COLOR_MODE == 4 || COLOR_MODE == 8 || COLOR_MODE == 24, "Invalid COLOR_MODE. Valid values are 4, 8, or 24 (default)."); - assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire Key, which should be able to hold an UTF8 code (4 bytes) or a terminal escape code (6 bytes)."); +previous_logger : (message: string, data: *void, info: Log_Info); + +module_logger :: (message: string, data: *void, info: Log_Info) { + // print("%0%0\n%0", Commands.MainScreenBuffer, message, Commands.AlternateScreenBuffer); + x, y := get_cursor_position(); + write_string(Commands.MainScreenBuffer); + previous_logger(message, data, info); + write_string(Commands.AlternateScreenBuffer); + set_cursor_position(x, y); } assert_is_active :: inline () { @@ -444,7 +455,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { } else { set_cursor_position(x, y); - for 0..chars_count print_character(#char "*"); + for 1..chars_count print_character(#char "*"); for chars_count..count_limit-1 print_character(#char " "); } set_cursor_position(x+idx, y); @@ -510,7 +521,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { start :: () { if active == true return; - + // TODO Should start() call flush_input internally? setup_key_map(); // TODO This is being called multiple times... please fix me! @@ -526,6 +537,9 @@ start :: () { Commands.SetUTF8, Commands.CursorNormalMode, Commands.KeypadNumMode); + + previous_logger = context.logger; + context.logger = module_logger; OS_prepare_terminal(); @@ -538,6 +552,9 @@ stop :: () { clear_style(); OS_reset_terminal(); + + context.logger = previous_logger; + write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); } diff --git a/modules/TUI/windows.jai b/modules/TUI/windows.jai index 5155a3f..5755ef4 100644 --- a/modules/TUI/windows.jai +++ b/modules/TUI/windows.jai @@ -72,21 +72,6 @@ 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; @@ -148,7 +133,7 @@ was_resized: bool; - windows_buffer: [512] u16; + widechar_buffer: [512] u16; peek_input :: inline () -> INPUT_RECORD, success := true { @@ -187,8 +172,6 @@ count_input :: inline () -> u32, success := true { #scope_export -// TODO All the log_error calls will be hidden by the terminal setup... we should store the logs internally, or write it to a file. - OS_prepare_terminal :: () { // stdin @@ -256,7 +239,7 @@ OS_reset_terminal :: () { } 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. */ @@ -295,19 +278,19 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success : } else { - chars_view: [] u16; - chars_view.data = windows_buffer.data; + widechar_view: [] u16; + widechar_view.data = widechar_buffer.data; - chars_to_read := ifx available_inputs <= windows_buffer.count then available_inputs else windows_buffer.count; + chars_to_read := ifx available_inputs <= widechar_buffer.count then available_inputs else widechar_buffer.count; - success = ReadConsoleW(stdin, chars_view.data, chars_to_read, *chars_view.count, null); + 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_error("Failed to read console: code %, %", error_code, error_string); return 0, false; } - result:, success = wide_to_utf8_new(chars_view.data, xx chars_view.count); + 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_error("Failed to convert from wide to utf8: code %, %", error_code, error_string); @@ -335,13 +318,12 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success : // 0: do not wait // -1: wait indefinitely OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool, success := true { - - /* 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. + /* + 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); diff --git a/ttt.jai b/ttt.jai index 13c9fcf..8dda204 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1157,7 +1157,7 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { main :: () { // -- -- -- Testing TUI -- START - + perform_test := false; assert_result :: (result: bool, error_message: string) { @@ -1187,6 +1187,31 @@ main :: () { assert_result(x == X && y == Y, "Failed set/get cursor position.\n"); } + if perform_test && 1 { + print("TEST : module logger\n", to_standard_error = true); + log("- log: before module start."); + TUI.start(); + + TUI.set_cursor_position(3, 3); + print("wait"); + sleep_milliseconds(1000); + log("- log: while module is active."); + sleep_milliseconds(1000); + print(" a bit"); + sleep_milliseconds(1000); + + #import "Windows"; + handle: HANDLE = ---; + initial_stdin_mode: u32; + if xx GetConsoleMode(handle, *initial_stdin_mode) == false { + error_code, error_string := get_error_value_and_string(); + log_error("- log: error code %, %", error_code, error_string); + } + + TUI.stop(); + log("- log: after module stop."); + } + if perform_test && 1 { print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); -- cgit v1.2.3 From bb1d53daa526d734fb2e33c2090f8396f87544ec Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 15 Apr 2024 16:50:42 +0100 Subject: Improved TUI logger; added snake example using TUI. --- modules/TUI/module.jai | 72 ++++++++++++----------- modules/TUI/unix.jai | 2 +- snake.jai | 157 +++++++++++++++++++++++++++++++++++++++++++++++++ ttt.jai | 10 ---- 4 files changed, 195 insertions(+), 46 deletions(-) create mode 100644 snake.jai (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 070161c..b5866d5 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -59,38 +59,44 @@ Drawings :: struct { } Commands :: struct { + // Screen buffers AlternateScreenBuffer :: "\e[?1049h"; - MainScreenBuffer :: "\e[?1049l"; + MainScreenBuffer :: "\e[?1049l"; + // Device. + Bell :: "\x07"; + QueryDeviceAttributes :: "\e[0c"; + + // Draw/text. DrawingMode :: "\e(0"; TextMode :: "\e(B"; ClearScreen :: "\e[2J"; ClearLine :: "\e[2K"; ClearScrollBack :: "\e[3J"; - - Bell :: "\x07"; - + SetGraphicsRendition :: "\e[%m"; + + // Character encoding. + EncodingIEC2022 :: "\e%@"; + EncodingUTF8 :: "\e%G"; + + // Window. SetWindowTitle :: "\e]0;%\e\\"; - - RefreshWindow :: "\e[7t"; // TODO Not yet tested. Is this required? + RefreshWindow :: "\e[7t"; + QueryWindowSizeInChars :: "\e[18t"; - SetIEC2022 :: "\e%@"; // TODO Remove this!? - SetUTF8 :: "\e%G"; // TODO Remove this!? - - SetGraphicsRendition :: "\e[%m"; - - // Cursor Position + // Cursor position. + SaveCursorPosition :: "\e7"; + RestoreCursorPosition :: "\e8"; SetCursorPosition :: "\e[%;%H"; - - // Cursor Visibility + QueryCursorPosition :: "\e[6n"; + + // Cursor visibility. ShowCursor :: "\e[?25h"; HideCursor :: "\e[?25l"; - StartBlinking :: "\e[?25h"; - StopBlinking :: "\e[?25l"; - SaveCursorPosition :: "\e7"; - RestoreCursorPosition :: "\e8"; - - // Cursor Shape + StartBlinking :: "\e[?12h"; + StopBlinking :: "\e[?12l"; + + // Cursor shape DefaultShape :: "\e[0 q"; BlinkingBlockShape :: "\e[1 q"; SteadyBlockShape :: "\e[2 q"; @@ -98,17 +104,12 @@ Commands :: struct { SteadyUnderlineShape :: "\e[4 q"; BlinkingBarShape :: "\e[5 q"; SteadyBarShape :: "\e[6 q"; - - // Input Mode + + // Input mode. KeypadAppMode :: "\e="; KeypadNumMode :: "\e>"; CursorAppMode :: "\e[?1h"; CursorNormalMode :: "\e[?1l"; - - // Query State - QueryCursorPosition :: "\e[6n"; // Emits the cursor position as: "ESC [ ; R" Where = row and = column. - QueryDeviceAttributes :: "\e[0c"; - QueryWindowSizeInChars :: "\e[18t"; // Emits the window size as: "ESC [ 8 ; t" Where = row and = column. TODO Does not work on windows. } #if COLOR_MODE == 4 { @@ -150,12 +151,16 @@ else { Style :: struct { #if COLOR_MODE == 4 || COLOR_MODE == 8 { - background: Palette = .BLACK; - foreground: Palette = .WHITE; + background: Palette; + foreground: Palette; } else { background: Color_24b; foreground: Color_24b; } + + background = Palette.BLACK; + foreground = Palette.WHITE; + bold: bool; underline: bool; strike_through: bool; @@ -279,12 +284,9 @@ input_override : Key; previous_logger : (message: string, data: *void, info: Log_Info); module_logger :: (message: string, data: *void, info: Log_Info) { - // print("%0%0\n%0", Commands.MainScreenBuffer, message, Commands.AlternateScreenBuffer); - x, y := get_cursor_position(); - write_string(Commands.MainScreenBuffer); + write_strings(Commands.SaveCursorPosition, Commands.MainScreenBuffer); previous_logger(message, data, info); - write_string(Commands.AlternateScreenBuffer); - set_cursor_position(x, y); + write_strings(Commands.AlternateScreenBuffer, Commands.RestoreCursorPosition); } assert_is_active :: inline () { @@ -534,7 +536,7 @@ start :: () { Commands.HideCursor, Commands.SaveCursorPosition, Commands.AlternateScreenBuffer, - Commands.SetUTF8, + Commands.EncodingUTF8, Commands.CursorNormalMode, Commands.KeypadNumMode); diff --git a/modules/TUI/unix.jai b/modules/TUI/unix.jai index 861fe11..7103d47 100644 --- a/modules/TUI/unix.jai +++ b/modules/TUI/unix.jai @@ -7,7 +7,7 @@ // Required to do unlocking input. libc :: #system_library "libc"; - // TODO Remote this. + // TODO Remove this. // cfmakeraw :: (termios: *Terminal_IO_Mode) -> void #foreign libc; /* https://elixir.bootlin.com/glibc/glibc-2.28/source/termios/cfmakeraw.c#L22 void diff --git a/snake.jai b/snake.jai new file mode 100644 index 0000000..ea8926f --- /dev/null +++ b/snake.jai @@ -0,0 +1,157 @@ +#import "Basic"; +#import "Random"; +TUI :: #import "TUI"; + +Vec2D :: struct { + x: int; + y: int; +} + +operator == :: (a: Vec2D, b: Vec2D) -> bool { + return a.x == b.x && a.y == b.y; +} + +screen_size_x: int = ---; +screen_size_y: int = ---; +player_name: string = ---; + +main :: () { + + game_loop :: () { + + LOOP_PERIOD_MS :: 30; + + score := 0; + dir := Vec2D.{1, 0}; + food := Vec2D.{5, 5}; + + random_food :: () -> Vec2D { + return 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) + }; + } + + snake_parts: [..] Vec2D; + for 0..13 array_add(*snake_parts, Vec2D.{3, 3}); + snake_parts[0].x += 1; + + TUI.flush_input(); + TUI.set_next_key(TUI.Keys.Resize); + timer := current_time_monotonic(); + while main_loop := true { + + timestamp := current_time_monotonic(); + key := TUI.get_key(LOOP_PERIOD_MS); + + if key == { + case TUI.Keys.Resize; + 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); + write_strings(" ", player_name, " "); + food = random_food(); + + case #char "q"; #through; + case #char "Q"; #through; + case TUI.Keys.Escape; + break main_loop; + + case TUI.Keys.Up; + if dir != Vec2D.{0, 1} then dir = Vec2D.{0, -1}; + + case TUI.Keys.Down; + if dir != Vec2D.{0, -1} then dir = Vec2D.{0, 1}; + + case TUI.Keys.Left; + if dir != Vec2D.{1, 0} then dir = Vec2D.{-1, 0}; + + case TUI.Keys.Right; + if dir != Vec2D.{-1, 0} then dir = Vec2D.{1, 0}; + } + + last_pos := snake_parts[snake_parts.count-1]; + + // Update 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; + + // 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 = random_food(); + } + + // 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, }); + TUI.set_cursor_position(food.x, food.y); + write_string(TUI.Drawings.Diamond); + } + write_string(TUI.Commands.TextMode); + + // Set score + TUI.set_cursor_position(3, 1); + print(" % ", score); + } + } + + GAME_OVER_TEXT :: "~ game over ~"; + INSTRUCTIONS_TEXT :: "(esc to exit)"; + + TUI.start(); + TUI.set_cursor_position(1, 1); + + write_string("Please enter player name: "); + player_name = TUI.read_input_line(64); + + while true { + game_loop(); + + // Game over screen. + TUI.draw_box(screen_size_x/3, screen_size_y/2-1, screen_size_x/3, 4); + TUI.set_cursor_position((screen_size_x-GAME_OVER_TEXT.count)/2, screen_size_y/2); + write_string(GAME_OVER_TEXT); + TUI.set_cursor_position((screen_size_x-GAME_OVER_TEXT.count)/2, screen_size_y/2+1); + write_string(INSTRUCTIONS_TEXT); + sleep_milliseconds(100); + TUI.flush_input(); + if TUI.get_key() == TUI.Keys.Escape then break; + } + + TUI.stop(); +} diff --git a/ttt.jai b/ttt.jai index 8dda204..8f39b56 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1191,7 +1191,6 @@ main :: () { print("TEST : module logger\n", to_standard_error = true); log("- log: before module start."); TUI.start(); - TUI.set_cursor_position(3, 3); print("wait"); sleep_milliseconds(1000); @@ -1199,15 +1198,6 @@ main :: () { sleep_milliseconds(1000); print(" a bit"); sleep_milliseconds(1000); - - #import "Windows"; - handle: HANDLE = ---; - initial_stdin_mode: u32; - if xx GetConsoleMode(handle, *initial_stdin_mode) == false { - error_code, error_string := get_error_value_and_string(); - log_error("- log: error code %, %", error_code, error_string); - } - TUI.stop(); log("- log: after module stop."); } -- cgit v1.2.3 From d7c2c312fe2ac08cadc3534a0a35357ef50cca20 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 30 Apr 2024 00:05:26 +0100 Subject: WIP : Add logger and cleanup TUI module. --- modules/TUI/module.jai | 38 ++++--- modules/TUI/unix.jai | 297 ++++++++++++++++++++++++++++-------------------- modules/TUI/windows.jai | 118 +++++++++---------- snake.jai | 7 +- ttt.jai | 42 +++---- 5 files changed, 285 insertions(+), 217 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 5fd29f6..121aa42 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -2,6 +2,9 @@ #scope_file +// - fix/implement/finish `TODO` on `TUI\module` (use some sort of buffet to reduce io/print calls) +// - fix/implement/finish `TODO` on `TUI\module` (use `dirty_bit_flag` to only update ehat has been changed) + #if OS == { case .LINUX; #load "unix.jai"; @@ -525,16 +528,17 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { return result, key; } -start :: () { +start :: () -> success := true #must { if active == true return; - // TODO Should start() call flush_input internally? - - setup_key_map(); // TODO This is being called multiple times... please fix me! - input_string.data = input_buffer.data; input_string.count = 0; input_override = xx Keys.None; + + previous_logger = context.logger; + context.logger = module_logger; + + setup_key_map(); write_strings( Commands.HideCursor, @@ -542,26 +546,34 @@ start :: () { Commands.AlternateScreenBuffer, Commands.EncodingUTF8, Commands.CursorNormalMode, - Commands.KeypadNumMode); - - previous_logger = context.logger; - context.logger = module_logger; + Commands.KeypadNumMode + ); - OS_prepare_terminal(); + if !OS_prepare_terminal() then return false; active = true; + + return; } -stop :: () { +stop :: () -> success := true #must { if active == false return; + active = false; clear_style(); - OS_reset_terminal(); + + if !OS_reset_terminal() then return false; context.logger = previous_logger; - write_strings(Commands.MainScreenBuffer, Commands.RestoreCursorPosition, Commands.ShowCursor); + write_strings( + Commands.MainScreenBuffer, + Commands.RestoreCursorPosition, + Commands.ShowCursor + ); + + return; } flush_input :: () { diff --git a/modules/TUI/unix.jai b/modules/TUI/unix.jai index 8eeb6c0..a6cd467 100644 --- a/modules/TUI/unix.jai +++ b/modules/TUI/unix.jai @@ -1,22 +1,171 @@ #scope_file /* -TODO : then log all error on unix... TODO : then do a good implementation of the libc functions about attributes... */ +USE_LIBC :: true; + #import "Atomics"; #import "System"; #import "POSIX"; + // Set Attributes Actions. + // 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 + // OptionalActions :: enum s32 { + TCSANOW :: 0; // Change immediately. + TCSADRAIN :: 1; // Change when pending output is written. + TCSAFLUSH :: 2; // Flush pending input before changing. + // } + + // TODO + // QueueSelector :: enum s32 { + // queue_selector + #if OS == { + case .LINUX; + // https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios.h + 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; + // https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html + TCIFLUSH :: 1; // Discard data received but not yet read. + TCOFLUSH :: 2; // Discard data written but not yet sent. + TCIOFLUSH :: 3; // Discard all pending data. + } + } + + // 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 + #if OS == { + case .LINUX; + NCCS :: 32; + + case .MACOS; + NCCS :: 20; + } + + // 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 + + + + // 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 : [NCCS]Control_Chars; // Control characters. + c_ispeed : u32; // Input speed (baud rates). + c_ospeed : u32; // Output speed (baud rates). + } + + // 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; + } + + +#if USE_LIBC { // 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; + + // 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 #foreign libc; tcsetattr :: (fd: s32, optional_actions: s32, termios_p : *Terminal_IO_Mode) -> s32 { - // TODO IMPLEMENT ME - + #if OS == .LINUX { TCSETS :: 0x5402; TCSETSW :: 0x5403; @@ -71,7 +220,6 @@ TODO : then do a good implementation of the libc functions about attributes... } // 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 { TCSETS :: 0x5402; TCSETSW :: 0x5403; @@ -135,128 +283,25 @@ TODO : then do a good implementation of the libc functions about attributes... } // 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; + was_resized : bool; + //////////////////////////////////////////////////////////////////////////////// -// 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; + if signal_code != SIGWINCH then return; atomic_swap(*was_resized, true); } } @@ -279,13 +324,14 @@ restore_resize_handler :: () { #scope_export -OS_prepare_terminal :: () -> success := true { // On critical paths, we may use the #must for the success return value. +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_error("Failed to get initial_tio_mode: code %, %", error_code, error_string); + return false; } raw_tio_mode = initial_tio_mode; @@ -297,7 +343,7 @@ OS_prepare_terminal :: () -> success := true { // On critical paths, we may use raw_tio_mode.c_cc[Control_Chars.VMIN] = 1; raw_tio_mode.c_cc[Control_Chars.VTIME] = 0; - error = tcsetattr(STDIN_FILENO, 0, *raw_tio_mode); + error = tcsetattr(STDIN_FILENO, TCSANOW, *raw_tio_mode); if error { error_code, error_string := get_error_value_and_string(); log_error("Failed to set raw_tio_mode: code %, %", error_code, error_string); @@ -311,7 +357,7 @@ OS_prepare_terminal :: () -> success := true { // On critical paths, we may use OS_reset_terminal :: inline () -> success := true { restore_resize_handler(); - error := tcsetattr(STDIN_FILENO, 0, *initial_tio_mode); + error := tcsetattr(STDIN_FILENO, TCSANOW, *initial_tio_mode); if error { error_code, error_string := get_error_value_and_string(); log_error("Failed to set initial_tio_mode: code %, %", error_code, error_string); @@ -321,7 +367,6 @@ OS_reset_terminal :: inline () -> success := true { } OS_flush_input :: inline () -> success := true { - TCIFLUSH :: 0; // TODO Is this always zero in all systems? error := tcflush(STDIN_FILENO, TCIFLUSH); if error { error_code, error_string := get_error_value_and_string(); @@ -331,7 +376,6 @@ OS_flush_input :: inline () -> success := true { return; } -// TODO Nothing is checking for the errors returned by this... shame! 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 { @@ -345,12 +389,21 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success : // timeout_milliseconds // 0: do not wait // -1: wait indefinitely -OS_wait_for_input :: (timeout_milliseconds: s32 = -1) -> is_input_available: bool { +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; - 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; + 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_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 :: () -> bool { diff --git a/modules/TUI/windows.jai b/modules/TUI/windows.jai index 5755ef4..608266d 100644 --- a/modules/TUI/windows.jai +++ b/modules/TUI/windows.jai @@ -9,47 +9,30 @@ CP_UTF8 :: 65001; // 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 :: u32; - LPDWORD :: *u32; - UINT :: u32; - - PINPUT_RECORD :: *INPUT_RECORD; - - // 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; + 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 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_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; // Makes mouse events available to the ReadConsoleInput function. - 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. + ENABLE_MOUSE_INPUT; + ENABLE_INSERT_MODE; _UNUSED_0040_; _UNUSED_0080_; _UNUSED_0100_; - ENABLE_VIRTUAL_TERMINAL_INPUT; // If enable, makes user input available to the ReadConsole function. + ENABLE_VIRTUAL_TERMINAL_INPUT; // If set, makes user input available to the ReadConsole function. _UNUSED_0400_; _UNUSED_0800_; } @@ -57,11 +40,11 @@ // 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; // + 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_; @@ -123,6 +106,23 @@ 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; @@ -135,6 +135,7 @@ widechar_buffer: [512] u16; +//////////////////////////////////////////////////////////////////////////////// peek_input :: inline () -> INPUT_RECORD, success := true { record: INPUT_RECORD; @@ -172,19 +173,19 @@ count_input :: inline () -> u32, success := true { #scope_export -OS_prepare_terminal :: () { +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_error("Invalid input handler: code %, %", error_code, error_string); - return; + return false; } if xx GetConsoleMode(stdin, *initial_stdin_mode) == false { error_code, error_string := get_error_value_and_string(); log_error("Failed to get input mode: code %, %", error_code, error_string); - return; + return false; } raw_stdin_mode = (cast(Console_Input_Mode) initial_stdin_mode); raw_stdin_mode |= (.ENABLE_VIRTUAL_TERMINAL_INPUT); @@ -193,7 +194,7 @@ OS_prepare_terminal :: () { if xx SetConsoleMode(stdin, xx raw_stdin_mode) == false { error_code, error_string := get_error_value_and_string(); log_error("Failed to set input mode: code %, %", error_code, error_string); - return; + return false; } // stdout @@ -201,12 +202,12 @@ OS_prepare_terminal :: () { if stdout == INVALID_HANDLE_VALUE { error_code, error_string := get_error_value_and_string(); log_error("Invalid output handler: code %, %", error_code, error_string); - return; + return false; } if xx GetConsoleMode(stdout, *initial_stdout_mode) == false { error_code, error_string := get_error_value_and_string(); log_error("Failed to get output mode: code %, %", error_code, error_string); - return; + 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); @@ -214,7 +215,7 @@ OS_prepare_terminal :: () { if xx SetConsoleMode(stdout, xx raw_stdout_mode) == false { error_code, error_string := get_error_value_and_string(); log_error("Failed to set output mode: code %, %", error_code, error_string); - return; + return false; } // Acording to [documentation](https://learn.microsoft.com/en-us/windows/win32/intl/code-pages) @@ -223,26 +224,27 @@ OS_prepare_terminal :: () { // 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 :: () { +OS_reset_terminal :: () -> success := true { if xx SetConsoleMode(stdin, initial_stdin_mode) == false { error_code, error_string := get_error_value_and_string(); log_error("Failed to reset input mode: code %, %", error_code, error_string); - return; + return false; } if xx SetConsoleMode(stdout, initial_stdout_mode) == false { error_code, error_string := get_error_value_and_string(); log_error("Failed to reset output mode: code %, %", error_code, error_string); - return; + return false; } + return; } OS_flush_input :: inline () { - /* - 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. - */ + // 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_error("Failed to flush input: code %, %", error_code, error_string); @@ -318,13 +320,11 @@ OS_read_input :: (buffer: *u8, bytes_to_read: s64) -> bytes_read: s64, success : // 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. - */ + // 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); diff --git a/snake.jai b/snake.jai index e3a15f6..465ae4d 100644 --- a/snake.jai +++ b/snake.jai @@ -136,8 +136,11 @@ main :: () { GAME_OVER_TEXT :: "~ game over ~"; INSTRUCTIONS_TEXT :: "(esc to exit)"; + + seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. + random_seed(seed); - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); TUI.set_cursor_position(1, 1); write_string("Please enter player name: "); @@ -158,5 +161,5 @@ main :: () { if TUI.get_key() == TUI.Keys.Escape then break; } - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); } diff --git a/ttt.jai b/ttt.jai index 8f39b56..45dc7f8 100644 --- a/ttt.jai +++ b/ttt.jai @@ -866,7 +866,7 @@ initialize_tui :: () { } } - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); } update_layout :: () { @@ -1165,7 +1165,7 @@ main :: () { print("- success\n", to_standard_error = true); } else { - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); print("- ERROR: %", error_message, to_standard_error = true); exit(1); } @@ -1178,19 +1178,19 @@ main :: () { if perform_test && 1 { print("TEST : set and get cursor position\n", to_standard_error = true); - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); X :: 2; Y :: 3; TUI.set_cursor_position(X, Y); x, y := TUI.get_cursor_position(); - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); assert_result(x == X && y == Y, "Failed set/get cursor position.\n"); } if perform_test && 1 { print("TEST : module logger\n", to_standard_error = true); log("- log: before module start."); - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); TUI.set_cursor_position(3, 3); print("wait"); sleep_milliseconds(1000); @@ -1198,14 +1198,14 @@ main :: () { sleep_milliseconds(1000); print(" a bit"); sleep_milliseconds(1000); - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); log("- log: after module stop."); } if perform_test && 1 { print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + assert(TUI.start(), "Failed to start 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."); @@ -1224,28 +1224,28 @@ main :: () { write_string(TUI.to_string(key)); } } - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); print("- success\n", to_standard_error = true); } if perform_test && 1 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + assert(TUI.start(), "Failed to start 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(); - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); assert_result(key == #char "y", "Failed to draw box.\n"); } if perform_test && 1 { print("TEST : get terminal size\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); TUI.clear_terminal(); width, height := TUI.get_terminal_size(); TUI.set_cursor_position(1, 1); @@ -1254,13 +1254,13 @@ main :: () { while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { key = TUI.get_key(); } - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); assert_result(key == #char "y", "Failed to get terminal size.\n"); } if perform_test && 1 { print("TEST : set terminal title\n", to_standard_error = true); - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); title := "BAZINGA"; TUI.set_terminal_title(title); TUI.set_cursor_position(1, 1); @@ -1269,14 +1269,14 @@ main :: () { while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { key = TUI.get_key(); } - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); assert_result(key == #char "y", "Failed to set terminal title.\n"); } if perform_test && 1 { print("TEST : print keys and set terminal title\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + assert(TUI.start(), "Failed to start TUI."); TUI.set_terminal_title("bazinga"); key: TUI.Key = #char "d"; last_none_char := "X"; @@ -1335,13 +1335,13 @@ main :: () { // set_temporary_storage_mark(__mark); } print("- success"); - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); } if perform_test && 1 { print("TEST : user input\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + assert(TUI.start(), "Failed to start 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):"); @@ -1366,14 +1366,14 @@ main :: () { } } answer := TUI.get_key(); - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); assert_result(answer == #char "y", error_message); } if perform_test && 1 { print("TEST : hidden user input\n", to_standard_error = true); auto_release_temp(); - TUI.start(); + assert(TUI.start(), "Failed to start 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):"); @@ -1397,7 +1397,7 @@ main :: () { } } answer := TUI.get_key(); - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); assert_result(answer == #char "y", error_message); } @@ -2018,7 +2018,7 @@ main :: () { TUI.get_key(); } - TUI.stop(); + assert(TUI.stop(), "Failed to stop TUI."); exit(xx ifx error_saving then 1 else 0); } -- cgit v1.2.3 From c7a4f1c0c98a1df1e683b3de0454b3d981519e9e Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 30 Apr 2024 02:31:41 +0100 Subject: WIP : OMG I add copied octal values from C which became decimal ones in jai. --- modules/TUI/unix.jai | 217 ++++++++++++++++++++++++++++++++------------------- ttt.jai | 1 + 2 files changed, 136 insertions(+), 82 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/unix.jai b/modules/TUI/unix.jai index a6cd467..bb22030 100644 --- a/modules/TUI/unix.jai +++ b/modules/TUI/unix.jai @@ -10,50 +10,45 @@ USE_LIBC :: true; #import "System"; #import "POSIX"; - // Set Attributes Actions. - // LINUX : https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios-tcflow.h + // 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 - // OptionalActions :: enum s32 { - TCSANOW :: 0; // Change immediately. - TCSADRAIN :: 1; // Change when pending output is written. - TCSAFLUSH :: 2; // Flush pending input before changing. - // } - - // TODO - // QueueSelector :: enum s32 { - // queue_selector + QueueSelector :: enum s32 { #if OS == { case .LINUX; - // https://sourceware.org/git/glibc.git -> ./sysdeps/unix/sysv/linux/bits/termios.h - TCIFLUSH :: 0; // Discard data received but not yet read. - TCOFLUSH :: 1; // Discard data written but not yet sent. - TCIOFLUSH :: 2; // Discard all pending data. + 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; - // https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html - TCIFLUSH :: 1; // Discard data received but not yet read. - TCOFLUSH :: 2; // Discard data written but not yet sent. - TCIOFLUSH :: 3; // Discard all pending data. + 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 + OptionalActions :: 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 - #if OS == { - case .LINUX; + Terminal_IO_Mode :: struct { + + #if OS == { + case .LINUX; NCCS :: 32; - case .MACOS; + case .MACOS; NCCS :: 20; - } + } - // 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 - - - - // 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. @@ -65,68 +60,126 @@ USE_LIBC :: true; } // 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 :: 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). + 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.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html 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. + #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.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html 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; + #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.h + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html 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. + #if OS == { + + case .LINUX; + ISIG :: 0x00000001; // Enable signals. + ICANON :: 0x00000002; // Do 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. + TOSTOP :: 0x00000100; // Send SIGTTOU for background output. + IEXTEN :: 0x00008000; // Enable DISCARD and LNEXT. + + case .MACOS; + ISIG :: 0x00000080; // Enable signals. + ICANON :: 0x00000100; // Do erase and kill processing. + 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; // Send SIGTTOU for background output. + IEXTEN :: 0x00000400; // Enable DISCARD and LNEXT. + } } // Control Characters + TODO WIP + // LINUX : ??? + // MACOS : https://opensource.apple.com/source/xnu/xnu-792/bsd/sys/termios.h.auto.html Control_Chars :: enum u8 { VINTR :: 0; VQUIT :: 1; @@ -185,13 +238,13 @@ else { k_termios: __kernel_termios; cmd: u64; if optional_actions == { - case xx SetAttributesActions.TCSANOW; + case xx OptionalActions.TCSANOW; cmd = TCSETS; - case xx SetAttributesActions.TCSADRAIN; + case xx OptionalActions.TCSADRAIN; cmd = TCSETSW; - case xx SetAttributesActions.TCSAFLUSH; + case xx OptionalActions.TCSAFLUSH; cmd = TCSETSF; case; @@ -343,7 +396,7 @@ OS_prepare_terminal :: () -> success := true { raw_tio_mode.c_cc[Control_Chars.VMIN] = 1; raw_tio_mode.c_cc[Control_Chars.VTIME] = 0; - error = tcsetattr(STDIN_FILENO, TCSANOW, *raw_tio_mode); + error = tcsetattr(STDIN_FILENO, xx OptionalActions.TCSANOW, *raw_tio_mode); if error { error_code, error_string := get_error_value_and_string(); log_error("Failed to set raw_tio_mode: code %, %", error_code, error_string); @@ -357,7 +410,7 @@ OS_prepare_terminal :: () -> success := true { OS_reset_terminal :: inline () -> success := true { restore_resize_handler(); - error := tcsetattr(STDIN_FILENO, TCSANOW, *initial_tio_mode); + error := tcsetattr(STDIN_FILENO, xx OptionalActions.TCSANOW, *initial_tio_mode); if error { error_code, error_string := get_error_value_and_string(); log_error("Failed to set initial_tio_mode: code %, %", error_code, error_string); @@ -367,7 +420,7 @@ OS_reset_terminal :: inline () -> success := true { } OS_flush_input :: inline () -> success := true { - error := tcflush(STDIN_FILENO, TCIFLUSH); + error := tcflush(STDIN_FILENO, xx QueueSelector.TCIFLUSH); if error { error_code, error_string := get_error_value_and_string(); log_error("Failed to flush input: code %, %", error_code, error_string); diff --git a/ttt.jai b/ttt.jai index 45dc7f8..63317d7 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1956,6 +1956,7 @@ main :: () { // Coalesce similar tasks. case #char "c"; #through; case #char "C"; + // TODO Active task is lost... 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; -- cgit v1.2.3 From 66dfee359cbe2c18ce8400edeb801a2373a771f4 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 2 May 2024 00:57:53 +0100 Subject: WIP : Using string builder to improve TUI performance. --- modules/TUI/module.jai | 87 ++++++++++++++++++++++++++++++++++++++++++++++---- ttt.jai | 2 ++ 2 files changed, 83 insertions(+), 6 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index f8b8265..991dc74 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -3,7 +3,6 @@ #scope_file // - fix/implement/finish `TODO` on `TUI\module` (use some sort of buffet to reduce io/print calls) -// - fix/implement/finish `TODO` on `TUI\module` (use `dirty_bit_flag` to only update ehat has been changed) #if OS == { case .LINUX; @@ -297,7 +296,7 @@ module_logger :: (message: string, data: *void, info: Log_Info) { } assert_is_active :: inline () { - assert(active, "TUI is not ready."); + assert(active, "TUI is not ready. Please call TUI.start() before."); // TODO Improve error message... and maybe use the logger and return success=false flag } //////////////////////////////////////////////////////////////////////////////// @@ -444,10 +443,6 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { str.count = 0; idx := 0; - // TODO Some of these may be nice to have: - // > https://unix.stackexchange.com/questions/255707/what-are-the-keyboard-shortcuts-for-the-command-line - // At least... add the Ctrl+u to clear the entire line. - x, y := get_cursor_position(); write_strings(Commands.ShowCursor, Commands.StartBlinking, Commands.BlinkingBarShape); @@ -531,6 +526,11 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { start :: () -> success := true #must { if active == true return; + + if inited == false { + init_string_builder(*temp_builder,, temp); // TODO Testing + inited = true; + } input_string.data = input_buffer.data; input_string.count = 0; @@ -583,7 +583,19 @@ flush_input :: () { input_string.count = 0; } + + +// Using String_Builder improves: +// - procedure time is now ~12x faster; +// - reduces CPU usage by ~ +average: float64 = 0; +counter: float64 = 0; +inited:=false; +temp_builder: String_Builder; + +#if false { draw_box :: (x: int, y: int, width: int, height: int) { + t0 := current_time_monotonic(); assert_is_active(); // TODO Check if using a String_Builder improves performance (measure it)! @@ -626,6 +638,69 @@ draw_box :: (x: int, y: int, width: int, height: int) { write_string(Drawings.CornerBR); write_string(Commands.TextMode); + + 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(">%<", average); +} + +} else { + + +// TODO Not sure how... but this seems to be leaking memory... maybe it's the String_Builder!? +draw_box :: (x: int, y: int, width: int, height: int) { + t0 := current_time_monotonic(); + + assert_is_active(); + + // 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(); + + builder := temp_builder; + // init_string_builder(*builder); + + append(*builder, Commands.DrawingMode); + append(*builder, tprint(Commands.SetCursorPosition, y, x)); + append(*builder, Drawings.CornerTL); + + for 1..width-2 { + append(*builder, Drawings.LineH); + } + append(*builder, Drawings.CornerTR); + + for idx: y+1..y+height-2 { + tmpL := tprint(Commands.SetCursorPosition, idx, x); + tmpR := tprint(Commands.SetCursorPosition, idx, x+width-1); + append(*builder, tmpL); + append(*builder, Drawings.LineV); + append(*builder, tmpR); + append(*builder, Drawings.LineV); + } + + append(*builder, tprint(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); + + write_string(builder_to_string(*builder)); + + 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(">%<", average); +} } // TODO Maybe rename to "clear()" diff --git a/ttt.jai b/ttt.jai index 63317d7..50fa198 100644 --- a/ttt.jai +++ b/ttt.jai @@ -28,6 +28,8 @@ #import "UTF8"; TUI :: #import "TUI"(COLOR_MODE=4); +// - fix/implement/finish TODO : use `dirty_bit_flag` to only update ehat has been changed + VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). -- cgit v1.2.3 From 8b312bad33617f9b718232141d2855d80cb8b912 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 3 May 2024 00:32:06 +0100 Subject: WIP : Cleaning up TUI module. --- modules/TUI/module.jai | 178 +++++++++++++------------------------------- modules/TUI/palette_24b.jai | 2 +- snake.jai | 6 +- ttt.jai | 51 ++++++------- unused.jai | 15 ++++ 5 files changed, 95 insertions(+), 157 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 991dc74..d44edab 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,9 +1,7 @@ -#module_parameters(COLOR_MODE := 24); +#module_parameters(COLOR_MODE_BITS := 24); #scope_file -// - fix/implement/finish `TODO` on `TUI\module` (use some sort of buffet to reduce io/print calls) - #if OS == { case .LINUX; #load "unix.jai"; @@ -22,14 +20,14 @@ #load "key_map.jai"; #run { - assert(COLOR_MODE == 4 || COLOR_MODE == 8 || COLOR_MODE == 24, "Invalid COLOR_MODE. Valid values are 4, 8, or 24 (default)."); + assert(COLOR_MODE_BITS == 4 || COLOR_MODE_BITS == 8 || COLOR_MODE_BITS == 24, "Invalid COLOR_MODE_BITS. Valid values are 4, 8, or 24 (default)."); assert(input_buffer.count >= KEY_SIZE, "The input buffer size must be capable to hold an entire Key, which should be able to hold an UTF8 code (4 bytes) or a terminal escape code (6 bytes)."); } #scope_export; -// Special Graphics Characters -Drawings :: struct { +// Special Graphics Characters. +Drawings :: struct #type_info_none { Blank :: "\x5F"; Diamond :: "\x60"; Checkerboard :: "\x61"; @@ -64,7 +62,9 @@ Drawings :: struct { CenteredDot :: "\x7E"; } -Commands :: struct { +// Terminal Escape Codes. +Commands :: struct #type_info_none { + // Screen buffers AlternateScreenBuffer :: "\e[?1049h"; MainScreenBuffer :: "\e[?1049l"; @@ -118,7 +118,7 @@ Commands :: struct { CursorNormalMode :: "\e[?1l"; } -#if COLOR_MODE == 4 { +#if COLOR_MODE_BITS == 4 { #load "palette_4b.jai"; set_colors :: inline (foreground: Palette, background: Palette) { @@ -127,7 +127,7 @@ Commands :: struct { cast(u8)foreground + 30, cast(u8)background + 40); } } -else #if COLOR_MODE == 8 { +else #if COLOR_MODE_BITS == 8 { #load "palette_8b.jai"; set_colors :: inline (foreground: Palette, background: Palette) { @@ -153,19 +153,21 @@ else { } } -#add_context tui_style: Style; - Style :: struct { - #if COLOR_MODE == 4 || COLOR_MODE == 8 { + #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; + + // TODO Make this work... + use_default_background_color := false; + use_default_foreground_color := false; bold: bool; underline: bool; @@ -198,9 +200,6 @@ using_style :: (style: Style) #expand { `defer set_style(__style); } -// 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. @@ -238,7 +237,7 @@ to_string :: inline (key: Key) -> string { // TODO FIXME TEMPORARY MEMORY return str; } -is_escape_code :: inline (key: Key) -> bool { +is_escape_code :: (key: Key) -> bool { beginsWithEscape := ((key & 0xFF) ^ #char "#") == 0; hasSomethingElse := (key & (~0xFF)) != 0; return beginsWithEscape && hasSomethingElse; @@ -281,6 +280,8 @@ Keys :: struct #type_info_none { F12 : Key : #run to_key("#f12"); } +#add_context tui_style: Style; + active := false; input_buffer : [1024] u8; @@ -296,7 +297,7 @@ module_logger :: (message: string, data: *void, info: Log_Info) { } assert_is_active :: inline () { - assert(active, "TUI is not ready. Please call TUI.start() before."); // TODO Improve error message... and maybe use the logger and return success=false flag + assert(active, "Please call TUI.setup_terminal() before using this procedure."); // TODO Improve error message... and maybe use the logger and return success=false flag } //////////////////////////////////////////////////////////////////////////////// @@ -376,12 +377,15 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return to_key(to_parse); } -// TODO Review me! -read_input :: (count_limit: int = -1, terminators: .. u8) -> string { +// TODO Review me and add some comments. +read_input :: (count_limit: int = -1, terminators: .. u8) -> string, success := true { assert_is_active(); - - assert(count_limit >= 0 || terminators.count > 0, "Infinite loop detected, aborting."); // TODO Maybe just return!? Or simply return success=false with #must - + + if count_limit < 0 && terminators.count <= 0 { + log_error("Invalid arguments passed to read_input(): (%).\n", count_limit); // TODO Improve error message. + return "", false; + } + if count_limit < 0 { builder: String_Builder; init_string_builder(*builder); @@ -436,7 +440,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, 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. */ - + assert_is_active(); assert(count_limit >= 0, "Invalid value on count_limit parameter."); // TODO Too agressive str := alloc_string(count_limit); @@ -452,7 +456,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { chars_count := count_characters(str); - // Preview input. + // Preview input. TODO Optimize using temp_builder if is_visible { set_cursor_position(x, y); write_string(str); @@ -524,13 +528,8 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { return result, key; } -start :: () -> success := true #must { +setup_terminal :: () -> success := true #must { if active == true return; - - if inited == false { - init_string_builder(*temp_builder,, temp); // TODO Testing - inited = true; - } input_string.data = input_buffer.data; input_string.count = 0; @@ -557,7 +556,7 @@ start :: () -> success := true #must { return; } -stop :: () -> success := true #must { +reset_terminal :: () -> success := true #must { if active == false return; active = false; @@ -578,138 +577,61 @@ stop :: () -> success := true #must { } flush_input :: () { + assert_is_active(); OS_flush_input(); input_string.data = input_buffer.data; input_string.count = 0; } - - -// Using String_Builder improves: -// - procedure time is now ~12x faster; -// - reduces CPU usage by ~ -average: float64 = 0; -counter: float64 = 0; -inited:=false; -temp_builder: String_Builder; - -#if false { +// TODO What if we return success and fail when input arguments are invalid? draw_box :: (x: int, y: int, width: int, height: int) { - t0 := current_time_monotonic(); - assert_is_active(); - - // 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 - ); + assert_is_active(); //TODO NOT NEEDED - 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); + if x <= 0 || y <= 0 || width <= 1 || height <= 1 { + log_error("Invalid arguments passed to draw_box(): (%, %, %, %).\n", x, y, width, height); // TODO Improve error message. + return; } - write_string(Drawings.CornerBR); - write_string(Commands.TextMode); - - 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(">%<", average); -} - -} else { - - -// TODO Not sure how... but this seems to be leaking memory... maybe it's the String_Builder!? -draw_box :: (x: int, y: int, width: int, height: int) { - t0 := current_time_monotonic(); - - assert_is_active(); - - // 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(); - builder := temp_builder; - // init_string_builder(*builder); - + builder := String_Builder.{ allocator = temporary_allocator }; + append(*builder, Commands.DrawingMode); + + // Draw top line append(*builder, tprint(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 { - tmpL := tprint(Commands.SetCursorPosition, idx, x); - tmpR := tprint(Commands.SetCursorPosition, idx, x+width-1); - append(*builder, tmpL); + append(*builder, tprint(Commands.SetCursorPosition, idx, x)); append(*builder, Drawings.LineV); - append(*builder, tmpR); + append(*builder, tprint(Commands.SetCursorPosition, idx, x+width-1)); append(*builder, Drawings.LineV); } + // Draw bottom line. append(*builder, tprint(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); write_string(builder_to_string(*builder)); - - 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(">%<", average); -} } -// TODO Maybe rename to "clear()" clear_terminal :: inline () { assert_is_active(); write_string(Commands.ClearScreen); } -// TODO Maybe rename to "get_size()" get_terminal_size :: () -> width: int, height: int { assert_is_active(); @@ -735,7 +657,7 @@ get_terminal_size :: () -> width: int, height: int { 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."); @@ -757,7 +679,7 @@ get_terminal_size :: () -> width: int, height: int { return columns, rows; } -set_cursor_position :: (x: int, y: int) { +set_cursor_position :: inline (x: int, y: int) { assert_is_active(); print(Commands.SetCursorPosition, y, x); } @@ -784,7 +706,7 @@ get_cursor_position :: () -> x: int, y: int { 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."); @@ -796,7 +718,7 @@ get_cursor_position :: () -> x: int, y: int { return column, row; } -set_terminal_title :: (title: string) { +set_terminal_title :: inline (title: string) { assert_is_active(); print(Commands.SetWindowTitle, title); } diff --git a/modules/TUI/palette_24b.jai b/modules/TUI/palette_24b.jai index 7a263a0..ea0191c 100644 --- a/modules/TUI/palette_24b.jai +++ b/modules/TUI/palette_24b.jai @@ -1,5 +1,5 @@ // https://www.ditig.com/publications/256-colors-cheat-sheet -Palette :: struct { +Palette :: struct #type_info_none { BLACK :: Color_24b.{0x00, 0x00, 0x00}; MAROON :: Color_24b.{0x80, 0x00, 0x00}; GREEN :: Color_24b.{0x00, 0x80, 0x00}; diff --git a/snake.jai b/snake.jai index 465ae4d..b35c0fb 100644 --- a/snake.jai +++ b/snake.jai @@ -1,6 +1,6 @@ #import "Basic"; #import "Random"; -TUI :: #import "TUI"(COLOR_MODE = 8); +TUI :: #import "TUI"(COLOR_MODE_BITS = 8); Vec2D :: struct { x: int; @@ -140,7 +140,7 @@ main :: () { seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. random_seed(seed); - assert(TUI.start(), "Failed to start TUI."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); TUI.set_cursor_position(1, 1); write_string("Please enter player name: "); @@ -161,5 +161,5 @@ main :: () { if TUI.get_key() == TUI.Keys.Escape then break; } - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); } diff --git a/ttt.jai b/ttt.jai index 50fa198..3512d20 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,7 +17,8 @@ // - release : jai ttt.jai -quiet -x64 -release // - debug : jai ttt.jai -quiet -x64 -#import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. +// #import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. +#import "Basic"; #import "System"; #import "Sort"; #import "Math"; @@ -26,7 +27,7 @@ #import "String"; #import "Integer_Saturating_Arithmetic"; #import "UTF8"; -TUI :: #import "TUI"(COLOR_MODE=4); +TUI :: #import "TUI"(COLOR_MODE_BITS=4); // - fix/implement/finish TODO : use `dirty_bit_flag` to only update ehat has been changed @@ -868,7 +869,7 @@ initialize_tui :: () { } } - assert(TUI.start(), "Failed to start TUI."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); } update_layout :: () { @@ -1167,7 +1168,7 @@ main :: () { print("- success\n", to_standard_error = true); } else { - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); print("- ERROR: %", error_message, to_standard_error = true); exit(1); } @@ -1180,19 +1181,19 @@ main :: () { if perform_test && 1 { print("TEST : set and get cursor position\n", to_standard_error = true); - assert(TUI.start(), "Failed to start TUI."); + 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.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); assert_result(x == X && y == Y, "Failed set/get cursor position.\n"); } if perform_test && 1 { print("TEST : module logger\n", to_standard_error = true); log("- log: before module start."); - assert(TUI.start(), "Failed to start TUI."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); TUI.set_cursor_position(3, 3); print("wait"); sleep_milliseconds(1000); @@ -1200,14 +1201,14 @@ main :: () { sleep_milliseconds(1000); print(" a bit"); sleep_milliseconds(1000); - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); log("- log: after module stop."); } if perform_test && 1 { print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); - assert(TUI.start(), "Failed to start TUI."); + 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."); @@ -1226,28 +1227,28 @@ main :: () { write_string(TUI.to_string(key)); } } - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); print("- success\n", to_standard_error = true); } if perform_test && 1 { print("TEST : draw box\n", to_standard_error = true); auto_release_temp(); - assert(TUI.start(), "Failed to start TUI."); + 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.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); assert_result(key == #char "y", "Failed to draw box.\n"); } if perform_test && 1 { print("TEST : get terminal size\n", to_standard_error = true); auto_release_temp(); - assert(TUI.start(), "Failed to start TUI."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); TUI.clear_terminal(); width, height := TUI.get_terminal_size(); TUI.set_cursor_position(1, 1); @@ -1256,13 +1257,13 @@ main :: () { while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { key = TUI.get_key(); } - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); assert_result(key == #char "y", "Failed to get terminal size.\n"); } if perform_test && 1 { print("TEST : set terminal title\n", to_standard_error = true); - assert(TUI.start(), "Failed to start TUI."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); title := "BAZINGA"; TUI.set_terminal_title(title); TUI.set_cursor_position(1, 1); @@ -1271,14 +1272,14 @@ main :: () { while (key == xx TUI.Keys.None || key == xx TUI.Keys.Resize) { key = TUI.get_key(); } - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); assert_result(key == #char "y", "Failed to set terminal title.\n"); } if perform_test && 1 { print("TEST : print keys and set terminal title\n", to_standard_error = true); auto_release_temp(); - assert(TUI.start(), "Failed to start TUI."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); TUI.set_terminal_title("bazinga"); key: TUI.Key = #char "d"; last_none_char := "X"; @@ -1337,13 +1338,13 @@ main :: () { // set_temporary_storage_mark(__mark); } print("- success"); - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); } if perform_test && 1 { print("TEST : user input\n", to_standard_error = true); auto_release_temp(); - assert(TUI.start(), "Failed to start TUI."); + 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):"); @@ -1368,14 +1369,14 @@ main :: () { } } answer := TUI.get_key(); - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); assert_result(answer == #char "y", error_message); } if perform_test && 1 { print("TEST : hidden user input\n", to_standard_error = true); auto_release_temp(); - assert(TUI.start(), "Failed to start TUI."); + 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):"); @@ -1399,14 +1400,14 @@ main :: () { } } answer := TUI.get_key(); - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); assert_result(answer == #char "y", error_message); } // -- -- -- Testing TUI -- STOP - defer report_memory_leaks(); // TODO Remove after final debug sessions. + // defer report_memory_leaks(); // TODO Remove after final debug sessions. defer free_memory(); @@ -1833,7 +1834,7 @@ main :: () { if (active_task == null) continue; select_task(db, db.active_idx); - // Start/Stop + // Start and stop. case TUI.Keys.Enter; #through; case TUI.Keys.Space; if (db != *database || selected_task == null) continue; @@ -2021,7 +2022,7 @@ main :: () { TUI.get_key(); } - assert(TUI.stop(), "Failed to stop TUI."); + assert(TUI.reset_terminal(), "Failed to reset TUI."); exit(xx ifx error_saving then 1 else 0); } diff --git a/unused.jai b/unused.jai index 0425f8d..a8dddfc 100644 --- a/unused.jai +++ b/unused.jai @@ -38,6 +38,21 @@ print_database :: (db: Database) { } } +// --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- // + + +// 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. -- cgit v1.2.3 From 675b0a5fea60dc33a97b0dc1871bb9a0b61818cc Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 4 May 2024 01:43:34 +0100 Subject: WIP : Cleanup TUI module. Finally decided to go with hard-asserts (vs soft-errors/logs). --- modules/TUI/module.jai | 70 ++++++-------- modules/TUI/tests.jai | 247 +++++++++++++++++++++++++++++++++++++++++++++++ modules/TUI/unix.jai | 2 +- ttt.jai | 255 +------------------------------------------------ 4 files changed, 282 insertions(+), 292 deletions(-) create mode 100644 modules/TUI/tests.jai (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 464b0d9..a6a3d11 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -120,6 +120,9 @@ Commands :: struct #type_info_none { CursorNormalMode :: "\e[?1l"; } +// TODO Check which procedures need the assert_is_active call. +// TODO Review the error messages on the asserts. + #if COLOR_MODE_BITS == 4 { #load "palette_4b.jai"; @@ -189,15 +192,15 @@ set_font_style :: inline (bold: bool, underline: bool = false, strike_through: b set_style :: (style: Style) { set_font_style(style.bold, style.underline, style.strike_through, style.negative); set_colors(style.foreground, style.background); - context.tui_style = style; + context.terminal_style = style; } -clear_style :: () { +clear_style :: inline () { write_string(#run sprint(Commands.SetGraphicsRendition, "0")); } using_style :: (style: Style) #expand { - __style := context.tui_style; + __style := context.terminal_style; set_style(style); `defer set_style(__style); } @@ -215,24 +218,23 @@ using_style :: (style: Style) #expand { Key :: u64; // Terminal key-codes have 1 to 6 bytes so we'll use 8 bytes. -to_key :: inline (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { +to_key :: (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { + assert(str.count <= KEY_SIZE, "Invalid argument passed to to_key(): 'str.count' must be less-than or equal to %, but it was %.", KEY_SIZE, str.count); + k: Key; - // #if DEBUG { - // assert(str.count <= 8); // TODO Add DEBUG to module parameters. - // } - for 0..str.count-1 #no_abc { + for 0..str.count-1 { k |= ((cast(u64)str[it]) << (it*8)); } return k; } -to_string :: inline (key: Key) -> string { // TODO FIXME TEMPORARY MEMORY - str := talloc_string(KEY_SIZE); +to_string :: (key: Key) -> string { + str := alloc_string(KEY_SIZE); str.count = 0; - while key != 0 #no_abc { - str[str.count] = xx key & 0xFF; - key >>= 8; + while key != 0 { str.count += 1; + str[str.count-1] = xx key & 0xFF; + key >>= 8; } return str; } @@ -280,7 +282,7 @@ Keys :: struct #type_info_none { F12 : Key : #run to_key("#f12"); } -#add_context tui_style: Style; +#add_context terminal_style: Style; active := false; @@ -297,7 +299,7 @@ module_logger :: (message: string, data: *void, info: Log_Info) { } assert_is_active :: inline () { - assert(active, "Please call TUI.setup_terminal() before using this procedure."); // TODO Improve error message... and maybe use the logger and return success=false flag + assert(active, "Please call setup_terminal() before using the module procedures."); } //////////////////////////////////////////////////////////////////////////////// @@ -309,6 +311,7 @@ set_next_key :: inline (key: Key) { input_override = key; } +// TODO Provide some documentation comments. get_key :: (timeout_milliseconds: s32 = -1) -> Key { assert_is_active(); @@ -377,15 +380,15 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { return to_key(to_parse); } -// TODO Review me and add some comments. -read_input :: (count_limit: int = -1, terminators: .. u8) -> string, success := true { +// 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 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(); - - if count_limit < 0 && terminators.count <= 0 { - log_error("Invalid arguments passed to read_input(): (%).\n", count_limit); // TODO Improve error message. - return "", false; - } - + assert(count_limit >= 0 || terminators.count > 0, "Invalid arguments passed to read_input() will result in infinite-loop."); + + // TODO Provide some documentation comments. if count_limit < 0 { builder: String_Builder; init_string_builder(*builder); @@ -439,13 +442,7 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string, success := // 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(); - - // TODO If we pass success... then, does it make sense to return success=true on resize? - assert(count_limit >= 0, "Invalid value passed to count_limit."); - // if count_limit < 0 { - // log_error("Invalid arguments passed to read_input_line(): (%, %).\n", count_limit, is_visible); - // return "", Keys.None, false; - // } + assert(count_limit >= 0, "Invalid value passed to count_limit(): %.", count_limit); builder := String_Builder.{ allocator = temporary_allocator }; @@ -510,7 +507,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { if is_escape_code(key) continue; buff_idx := map_character_to_buffer_idx(str, idx); - key_str := to_string(key); + key_str := to_string(key,, allocator = temporary_allocator); // 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; @@ -538,7 +535,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { setup_terminal :: () -> success := true #must { if active == true return; - + input_string.data = input_buffer.data; input_string.count = 0; input_override = xx Keys.None; @@ -591,14 +588,9 @@ flush_input :: () { input_string.count = 0; } -// TODO What if we return success and fail when input arguments are invalid? draw_box :: (x: int, y: int, width: int, height: int) { - assert_is_active(); //TODO NOT NEEDED - - if x <= 0 || y <= 0 || width <= 1 || height <= 1 { - log_error("Invalid arguments passed to draw_box(): (%, %, %, %).\n", x, y, width, height); // TODO Improve error message. - return; - } + assert_is_active(); + assert(x > 0 && y > 0 && width > 1 && height > 1, "Invalid arguments passed to draw_box(): (%, %, %, %).\n", x, y, width, height); auto_release_temp(); diff --git a/modules/TUI/tests.jai b/modules/TUI/tests.jai new file mode 100644 index 0000000..fe213cb --- /dev/null +++ b/modules/TUI/tests.jai @@ -0,0 +1,247 @@ +// 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 : module logger\n", to_standard_error = true); + log("- log: before module start."); + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.set_cursor_position(3, 3); + print("wait"); + sleep_milliseconds(500); + log("- log: while module is active."); + sleep_milliseconds(500); + print(" a bit"); + sleep_milliseconds(1000); + assert(TUI.reset_terminal(), "Failed to reset TUI."); + log("- log: after module stop."); + } + + 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 = #char "d"; + 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)"); + } + + 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 index a8e0edf..940ac80 100644 --- a/modules/TUI/unix.jai +++ b/modules/TUI/unix.jai @@ -195,7 +195,7 @@ 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; + tcflush :: (fd: s32, queue_selector: s32) -> s32 #foreign libc; //////////////////////////////////////////////////////////////////////////////// diff --git a/ttt.jai b/ttt.jai index 3512d20..d471661 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,8 +17,7 @@ // - release : jai ttt.jai -quiet -x64 -release // - debug : jai ttt.jai -quiet -x64 -// #import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. -#import "Basic"; +#import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. #import "System"; #import "Sort"; #import "Math"; @@ -1107,7 +1106,7 @@ read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> val write_string(TUI.Commands.TextMode); TUI.set_cursor_position(x, y); - style_input := context.tui_style; + style_input := context.terminal_style; style_input.underline = true; TUI.using_style(style_input); @@ -1158,256 +1157,8 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { } main :: () { - - // -- -- -- Testing TUI -- START - - perform_test := false; - - 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 perform_test && 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 perform_test && 1 { - print("TEST : module logger\n", to_standard_error = true); - log("- log: before module start."); - assert(TUI.setup_terminal(), "Failed to setup TUI."); - TUI.set_cursor_position(3, 3); - print("wait"); - sleep_milliseconds(1000); - log("- log: while module is active."); - sleep_milliseconds(1000); - print(" a bit"); - sleep_milliseconds(1000); - assert(TUI.reset_terminal(), "Failed to reset TUI."); - log("- log: after module stop."); - } - - if perform_test && 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 perform_test && 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 perform_test && 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 perform_test && 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 perform_test && 1 { - print("TEST : print keys and set terminal title\n", to_standard_error = true); - auto_release_temp(); - assert(TUI.setup_terminal(), "Failed to setup TUI."); - TUI.set_terminal_title("bazinga"); - key: TUI.Key = #char "d"; - 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_string(last_none_char); - } - - 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; - for 0..str.count-1 { - tmp := tprint("%", FormatInt.{value = cast(u8)str[it], base=16},, temporary_allocator); - array_add(*array_to_print, tmp); - } - string_to_print := join(..array_to_print, separator = " "); - print(": % : ", string_to_print); - 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 perform_test && 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 perform_test && 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 - - // defer report_memory_leaks(); // TODO Remove after final debug sessions. + defer report_memory_leaks(); // TODO Remove after final debug sessions. defer free_memory(); -- cgit v1.2.3 From cb6c3caaa285bc8bd12c356d57bbfad86d70c0eb Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 5 May 2024 00:31:46 +0100 Subject: WIP : Cleanup TUI module scope. --- modules/TUI/module.jai | 222 +++++++++++++++++++++++--------------------- modules/TUI/palette_24b.jai | 6 ++ ttt.jai | 2 +- 3 files changed, 124 insertions(+), 106 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 772b30c..ab2567d 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,7 +1,9 @@ #module_parameters(COLOR_MODE_BITS := 24); + #scope_file + #if OS == { case .LINUX; #load "unix.jai"; @@ -26,8 +28,27 @@ 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, which should be able to hold an UTF8 code (4 bytes) or a terminal escape code (6 bytes)."); } +active := false; +input_buffer : [1024] u8; +input_string : string; +input_override : Key; + +previous_logger : (message: string, data: *void, info: Log_Info); + +module_logger :: (message: string, data: *void, info: Log_Info) { + write_strings(Commands.SaveCursorPosition, Commands.MainScreenBuffer); + previous_logger(message, data, info); + write_strings(Commands.AlternateScreenBuffer, Commands.RestoreCursorPosition); +} + +assert_is_active :: inline () { + assert(active, "Please call setup_terminal() to start using this module."); +} + + #scope_export; + // Special Graphics Characters. Drawings :: struct #type_info_none { Blank :: "\x5F"; @@ -123,6 +144,12 @@ Commands :: struct #type_info_none { // TODO Check which procedures need the assert_is_active call. // TODO Review the error messages on the asserts. +//////////////////////////////////////////////////////////////////////////////// + + +#scope_file + + #if COLOR_MODE_BITS == 4 { #load "palette_4b.jai"; @@ -144,12 +171,6 @@ else #if COLOR_MODE_BITS == 8 { else { #load "palette_24b.jai"; - Color_24b :: struct { - r: u8; - g: u8; - b: u8; - } - set_colors :: inline (foreground: Color_24b, background: Color_24b) { print( #run sprint(Commands.SetGraphicsRendition, "38;2;%;%;%;48;2;%;%;%"), @@ -158,6 +179,19 @@ else { } } +set_font_style :: inline (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); +} + + +#scope_export + + Style :: struct { #if COLOR_MODE_BITS == 4 || COLOR_MODE_BITS == 8 { background: Palette; @@ -180,16 +214,9 @@ Style :: struct { negative: bool; } -set_font_style :: inline (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); -} +#add_context terminal_style: Style; -set_style :: (style: Style) { +set_style :: inline (style: Style) { set_font_style(style.bold, style.underline, style.strike_through, style.negative); set_colors(style.foreground, style.background); context.terminal_style = style; @@ -205,6 +232,9 @@ using_style :: (style: Style) #expand { `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. @@ -216,34 +246,7 @@ using_style :: (style: Style) #expand { 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. - -to_key :: (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { - assert(str.count <= KEY_SIZE, "Invalid argument passed to to_key(): 'str.count' must be less-than or equal to %, but it was %.", KEY_SIZE, str.count); - - 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; -} +Key :: u64; Keys :: struct #type_info_none { None : Key : #run to_key("#none"); @@ -282,29 +285,86 @@ Keys :: struct #type_info_none { F12 : Key : #run to_key("#f12"); } -#add_context terminal_style: Style; +to_key :: (str: $T) -> Key #modify { return T == ([]u8) || T == string; } { + assert(str.count <= KEY_SIZE, "Invalid argument passed to to_key(): 'str.count' must be less-than or equal to %, but it was %.", KEY_SIZE, str.count); + + k: Key; + for 0..str.count-1 { + k |= ((cast(u64)str[it]) << (it*8)); + } + return k; +} -active := false; +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; +} -input_buffer : [1024] u8; -input_string : string; -input_override : Key; +is_escape_code :: (key: Key) -> bool { + beginsWithEscape := ((key & 0xFF) ^ #char "#") == 0; + hasSomethingElse := (key & (~0xFF)) != 0; + return beginsWithEscape && hasSomethingElse; +} -previous_logger : (message: string, data: *void, info: Log_Info); +//////////////////////////////////////////////////////////////////////////////// -module_logger :: (message: string, data: *void, info: Log_Info) { - write_strings(Commands.SaveCursorPosition, Commands.MainScreenBuffer); - previous_logger(message, data, info); - write_strings(Commands.AlternateScreenBuffer, Commands.RestoreCursorPosition); +is_active :: inline () -> bool { + return active; } -assert_is_active :: inline () { - assert(active, "Please call setup_terminal() to start using this 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; + + previous_logger = context.logger; + context.logger = module_logger; + + 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; } -//////////////////////////////////////////////////////////////////////////////// +reset_terminal :: () -> success := true #must { + if active == false return; + + active = false; -// #scope_export TODO Setup the scope_export and scope_file + clear_style(); + + if !OS_reset_terminal() then return false; + + context.logger = previous_logger; + + write_strings( + Commands.MainScreenBuffer, + Commands.RestoreCursorPosition, + Commands.ShowCursor + ); + + return; +} set_next_key :: inline (key: Key) { assert_is_active(); @@ -535,54 +595,6 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { return result, key; } -setup_terminal :: () -> success := true #must { - if active == true return; - - input_string.data = input_buffer.data; - input_string.count = 0; - input_override = xx Keys.None; - - previous_logger = context.logger; - context.logger = module_logger; - - 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; -} - -reset_terminal :: () -> success := true #must { - if active == false return; - - active = false; - - clear_style(); - - if !OS_reset_terminal() then return false; - - context.logger = previous_logger; - - write_strings( - Commands.MainScreenBuffer, - Commands.RestoreCursorPosition, - Commands.ShowCursor - ); - - return; -} - flush_input :: () { assert_is_active(); OS_flush_input(); diff --git a/modules/TUI/palette_24b.jai b/modules/TUI/palette_24b.jai index ea0191c..7545a0b 100644 --- a/modules/TUI/palette_24b.jai +++ b/modules/TUI/palette_24b.jai @@ -1,4 +1,10 @@ // 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}; diff --git a/ttt.jai b/ttt.jai index d471661..f3f1905 100644 --- a/ttt.jai +++ b/ttt.jai @@ -124,7 +124,7 @@ error_time_limit := Apollo_Time.{0, 0}; print_error :: (format :string, args : .. Any) { - if TUI.active == false { + if TUI.is_active() == false { print(format, ..args, to_standard_error = true); print("\n"); return; -- cgit v1.2.3 From 1ae2933bcbda43d16d2583a28c3c5a74adaaa9e4 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 5 May 2024 03:22:42 +0100 Subject: Implemented default background and foreground colors. --- modules/TUI/module.jai | 36 +++++++++++++++++++++++------------- ttt.jai | 15 +++++++++++++-- 2 files changed, 36 insertions(+), 15 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 07d121f..3f824a4 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -165,7 +165,7 @@ Style :: struct { background = Palette.BLACK; foreground = Palette.WHITE; - // TODO Make this work... + // TODO Make this work... and now that is implemented... test it. use_default_background_color := false; use_default_foreground_color := false; @@ -176,31 +176,41 @@ Style :: struct { } set_style :: inline (style: Style) { - print( - #run sprint(Commands.SetGraphicsRendition, "%;%;%;%"), - ifx style.bold then 1 else 22, - ifx style.underline then 4 else 24, - ifx style.strike_through then 9 else 29, - ifx style.negative then 7 else 27); + auto_release_temp(); + builder := String_Builder.{ allocator = temporary_allocator }; + #if COLOR_MODE_BITS == { case 4; - print( + print_to_builder(*builder, #run sprint("%0%0", Commands.SetGraphicsRendition, Commands.SetGraphicsRendition), - cast(u8)style.foreground + 30, cast(u8)style.background + 40); + cast(u8)style.foreground + 30, cast(u8)style.background + 40 + ); case 8; - print( + print_to_builder(*builder, #run sprint(Commands.SetGraphicsRendition, "38;5;%;48;5;%"), - cast(u8)style.foreground, cast(u8)style.background); + cast(u8)style.foreground, cast(u8)style.background + ); case 24; - print( + 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); + 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")); } + write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + context.terminal_style = style; } diff --git a/ttt.jai b/ttt.jai index f3f1905..0715544 100644 --- a/ttt.jai +++ b/ttt.jai @@ -17,7 +17,12 @@ // - release : jai ttt.jai -quiet -x64 -release // - debug : jai ttt.jai -quiet -x64 +DEBUG_MEMORY :: true; +#if DEBUG_MEMORY { #import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. +} else { +#import "Basic"; +} #import "System"; #import "Sort"; #import "Math"; @@ -1157,8 +1162,10 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { } main :: () { - + +#if DEBUG_MEMORY { defer report_memory_leaks(); // TODO Remove after final debug sessions. +} defer free_memory(); @@ -1774,7 +1781,11 @@ main :: () { } assert(TUI.reset_terminal(), "Failed to reset TUI."); - + +#if DEBUG_MEMORY { + return; +} else { exit(xx ifx error_saving then 1 else 0); } +} -- cgit v1.2.3 From 54103773365e06d6da8518d135c396949e683960 Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 5 May 2024 04:19:06 +0100 Subject: Fixed memory leaks. --- modules/TUI/module.jai | 26 +++++++++++++------------- ttt.jai | 32 ++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 27 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 3f824a4..dc9643c 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -511,17 +511,17 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { // Draw preview. if is_visible { - append(*builder, tprint(Commands.SetCursorPosition, y, x)); + print_to_builder(*builder, Commands.SetCursorPosition, y, x); append(*builder, str); for chars_count..count_limit-1 append(*builder, " "); } else { - append(*builder, tprint(Commands.SetCursorPosition, y, x)); + print_to_builder(*builder, Commands.SetCursorPosition, y, x); for 1..chars_count append(*builder, "*"); for chars_count..count_limit-1 append(*builder, " "); } - append(*builder, tprint(Commands.SetCursorPosition, y, x+idx)); - write_string(builder_to_string(*builder)); + print_to_builder(*builder, Commands.SetCursorPosition, y, x+idx); + write_string(builder_to_string(*builder,, allocator = temporary_allocator)); // Process input key. key = get_key(); @@ -600,7 +600,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { append(*builder, Commands.DrawingMode); // Draw top line - append(*builder, tprint(Commands.SetCursorPosition, y, x)); + print_to_builder(*builder, Commands.SetCursorPosition, y, x); append(*builder, Drawings.CornerTL); for 1..width-2 { append(*builder, Drawings.LineH); @@ -609,14 +609,14 @@ draw_box :: (x: int, y: int, width: int, height: int) { // Draw left and right sides. for idx: y+1..y+height-2 { - append(*builder, tprint(Commands.SetCursorPosition, idx, x)); + print_to_builder(*builder, Commands.SetCursorPosition, idx, x); append(*builder, Drawings.LineV); - append(*builder, tprint(Commands.SetCursorPosition, idx, x+width-1)); + print_to_builder(*builder, Commands.SetCursorPosition, idx, x+width-1); append(*builder, Drawings.LineV); } // Draw bottom line. - append(*builder, tprint(Commands.SetCursorPosition, y+height-1, x)); + print_to_builder(*builder, Commands.SetCursorPosition, y+height-1, x); append(*builder, Drawings.CornerBL); for 1..width-2 { append(*builder, Drawings.LineH); @@ -625,7 +625,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { append(*builder, Commands.TextMode); - write_string(builder_to_string(*builder)); + write_string(builder_to_string(*builder,, allocator = temporary_allocator)); } clear_terminal :: inline () { @@ -647,7 +647,7 @@ get_terminal_size :: () -> width: int, height: int { // Expected response format: \e[8;;t // where is the number of rows and of columns. FORMAT :: "\e[8;;t"; - input := read_input(64, #char "t",, temporary_allocator); + input := read_input(64, #char "t",, allocator = temporary_allocator); // Discard head noise. while input.count >= 3 && (input[0] != FORMAT[0] || input[1] != FORMAT[1] || input[2] != FORMAT[2]) { @@ -663,7 +663,7 @@ get_terminal_size :: () -> width: int, height: int { 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, ";",, temporary_allocator); + parts := split(input, ";",, allocator = temporary_allocator); rows = parse_int(*parts[1]); columns = parse_int(*parts[2]); } @@ -696,7 +696,7 @@ get_cursor_position :: () -> x: int, y: int { // Expected response format: \e[;R // where is the number of rows and of columns. FORMAT :: "\e[;R"; - input := read_input(64, #char "R"); + input := read_input(64, #char "R",, allocator = temporary_allocator); // Discard head noise. while input.count >= 2 && (input[0] != FORMAT[0] || input[1] != FORMAT[1]) { @@ -713,7 +713,7 @@ get_cursor_position :: () -> x: int, y: int { "Failed to query cursor position: invalid response."); advance(*input, 2); - parts := split(input, ";",, temporary_allocator); + parts := split(input, ";",, allocator = temporary_allocator); row := parse_int(*parts[0]); column := parse_int(*parts[1]); return column, row; diff --git a/ttt.jai b/ttt.jai index 0715544..929a736 100644 --- a/ttt.jai +++ b/ttt.jai @@ -590,12 +590,14 @@ load_database :: (db: *Database, path: string) -> success: bool { // Returns success. export_to_csv :: (db: Database, path: string) -> success: bool { 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 = ",")); + print_to_builder(*builder, "%\n", join(..CSV_HEADER, separator = ",",, temporary_allocator)); buffer: [Task.name.count] u8; name: string = xx buffer; @@ -701,15 +703,16 @@ import_from_csv :: (db: *Database, path: string) -> bool { } { // Parse CSV lines. - auto_release_temp(); // TODO Needs to be tested. line := csv; while (true) { + auto_release_temp(); + line, success := consume_next_line(*csv); // line, success := next_line(*csv); if success == false break; task: Task; - csv_values := split(line, ",",, temporary_allocator); // TODO Temporary memory... if file is too big this may break. + csv_values := split(line, ",",, temporary_allocator); // Import task name. name_length := min(task.name.count, csv_values[0].count); @@ -891,6 +894,8 @@ update_layout :: () { draw_tui :: (db: *Database, layout: *Layout) { + // TODO During dirty_flag revamp...we may also start using the String_Builder. + adjust_first_day_of_week := int.[ (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, @@ -990,7 +995,6 @@ draw_tui :: (db: *Database, layout: *Layout) { // Display up to rows allowed by the layout, or less if reached end of database. idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); for task_idx: idx_start..idx_stop-1 { - auto_release_temp(); // TODO Temporary memory being trashed?! task := *db.tasks[task_idx]; y += 1; x = 1; @@ -1138,7 +1142,7 @@ read_input_int :: (y: int, message: string) -> value: int, success: bool { input_pos_x := x + message.count + 2; input_width := size_x - input_pos_x; - str := read_input_string(input_pos_x, y, input_width); + str := read_input_string(input_pos_x, y, input_width,, temporary_allocator); value, success := parse_int(*str); return value, success; @@ -1170,7 +1174,6 @@ main :: () { defer free_memory(); { // Initialize app directory. - auto_release_temp(); home_dir, success_dir := get_home_directory(); // Returns system owned memory. if success_dir == false { home_dir = "."; @@ -1195,7 +1198,6 @@ main :: () { } { // Initialize database and archive files if needed. - auto_release_temp(); if (file_exists(db_file_path) == false) { if (store_database(database, db_file_path) == false) { print_error("Failed to initialize database."); @@ -1333,7 +1335,8 @@ main :: () { } if is_exit_requested { - exit(0); + // exit(0); // TODO fucking exit does not report memory leaks. + return; } } @@ -1353,7 +1356,9 @@ main :: () { 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) { @@ -1366,7 +1371,6 @@ main :: () { draw_error_window(); } - reset_temporary_storage(); key := TUI.get_key(INPUT_TIMEOUT_MS); if key == #char "q" || key == #char "Q" break; update_times(*database); @@ -1456,7 +1460,7 @@ main :: () { // 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); + input := read_input_string(2, selected_task_row, Task.name.count, size_x - 2 - Task.name.count,, temporary_allocator); if is_empty_string(input) == false { replace_chars(input, "\t\x0B\x0C\r", #char " "); // Replace weird spaces with space. memset(selected_task.name.data, 0, Task.name.count); @@ -1492,9 +1496,9 @@ main :: () { 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"); // TODO DAM this cast... + 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 { @@ -1504,7 +1508,7 @@ main :: () { // Get input string. TUI.using_style(action_style); - input := read_input_string(input_pos_x, selected_task_row, input_width); // TODO Temp stringzes. + input := read_input_string(input_pos_x, selected_task_row, input_width,, temporary_allocator); // Abort if input if empty. if is_empty_string(input) continue; -- cgit v1.2.3 From b32a697e1898a37497cfc504a2746dcf1f0710da Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 5 May 2024 15:55:30 +0100 Subject: Finished first version of TUI. --- modules/TUI/module.jai | 5 ++--- ttt.jai | 13 +++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 27c2fcc..e25672f 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -22,7 +22,7 @@ #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)."); } @@ -165,7 +165,6 @@ Style :: struct { background = Palette.BLACK; foreground = Palette.WHITE; - // TODO Make this work... and now that is implemented... test it. use_default_background_color := false; use_default_foreground_color := false; @@ -216,7 +215,7 @@ set_style :: (style: Style) { clear_style :: () { write_string(#run sprint(Commands.SetGraphicsRendition, "0")); - // TODO Maybe reset context.terminal_style? + context.terminal_style = .{ }; } using_style :: (style: Style) #expand { diff --git a/ttt.jai b/ttt.jai index 929a736..3f7ac59 100644 --- a/ttt.jai +++ b/ttt.jai @@ -86,6 +86,7 @@ pos_y : int; style_default := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.WHITE, + // use_default_background_color = true, TODO }; style_selected := TUI.Style.{ @@ -97,12 +98,14 @@ style_selected_inverted := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.CYAN, bold = true, + // use_default_background_color = true, TODO }; style_active := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.BLUE, bold = true, + // use_default_background_color = true, TODO }; style_active_selected := TUI.Style.{ @@ -115,6 +118,7 @@ style_error := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.RED, bold = true, + // use_default_background_color = true, TODO }; Layouts :: enum u8 { @@ -1009,6 +1013,9 @@ draw_tui :: (db: *Database, layout: *Layout) { else if (task == active_task) { TUI.set_style(style_active); } + else { + TUI.set_style(style_default); + } // Task title. x += 1; @@ -1043,10 +1050,8 @@ draw_tui :: (db: *Database, layout: *Layout) { // Task total. x += 1; print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); - - // Reset theme. - TUI.clear_style(); } + TUI.set_style(style_default); /////////////////////////////////////////////////////////////////////////// @@ -1099,7 +1104,7 @@ free_memory :: () { free(app_directory); free(db_file_path); free(ar_file_path); - //reset_temporary_storage(); + // reset_temporary_storage(); // TODO Not needed... I guess. } read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> value: string, success: bool { -- cgit v1.2.3 From 65227c476071914b82720084a2a775b44e4de885 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 6 May 2024 12:50:22 +0100 Subject: Cleanup UTF helper. --- modules/TUI/module.jai | 6 ++--- modules/UTF8.jai | 60 ++++++++++++++++++++++++++------------------------ ttt.jai | 11 +++++---- 3 files changed, 39 insertions(+), 38 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index e25672f..7c3a71d 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -398,7 +398,7 @@ get_key :: (timeout_milliseconds: s32 = -1) -> Key { // By default, parse a single UTF8 character (1 to 4 bytes). to_parse := input_string; - to_parse.count = count_utf8_bytes(input_string[0]); + 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. @@ -555,7 +555,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { case; if is_escape_code(key) continue; - buff_idx := map_character_to_buffer_idx(str, idx); + buff_idx := get_byte_idx(str, idx); key_str := to_string(key,, allocator = temporary_allocator); // Make sure we have space to append the new character at the end (in case we're trying to do it). @@ -572,7 +572,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { idx += 1; // Truncate string to avoid incomplete utf8 codes on the string tail. - str.count = truncate_string(str, count_limit); + truncate(*str, count_limit); } } diff --git a/modules/UTF8.jai b/modules/UTF8.jai index eba4585..b583809 100644 --- a/modules/UTF8.jai +++ b/modules/UTF8.jai @@ -1,25 +1,29 @@ -// BBBB BBBB & 1100 0000 == 10XX XXXX -> is continuation byte -// TODO Maybe rename to: is_continuation_byte -is_utf8_continuation_byte :: inline (byte: u8) -> bool { +// 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; } -// 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 -// TODO Maybe rename to: count_character_bytes -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; +// Given a leading_byte, returns the number of bytes on the character. +count_character_bytes :: inline (leading_byte: u8) -> int { + // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte + if (leading_byte & 0xE0) == 0xC0 return 1+1; + + // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte + if (leading_byte & 0xF0) == 0xE0 return 1+2; + + // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte + if (leading_byte & 0xF8) == 0xF0 return 1+3; + return 1; } -// Truncates the string to the length provided or shorter, in case of UTF8 strings that require so. -// Truncation is done by zeroing the tail of the string in place. -// Returns length of truncated string. -// TODO Maybe rename to: truncate -truncate_string :: (str: string, length: int) -> length: int { +// Truncates the string to the provided length and zeroes the discarded bytes. +// Returns the length of truncated string or -1 if string has no data. +truncate :: (str: *string, length: int) -> length: int { if str.data == null then return -1; if str.count < length then length = str.count; @@ -29,7 +33,7 @@ truncate_string :: (str: string, length: int) -> length: int { // Find index of first continuation byte. idx := length; - while (idx > 0 && is_utf8_continuation_byte(data[idx - 1])) { + while (idx > 0 && is_continuation_byte(data[idx - 1])) { idx -= 1; } continuation_bytes := length - idx; @@ -50,13 +54,12 @@ truncate_string :: (str: string, length: int) -> length: int { } memset(data + length, 0, count - length); - // str.count = length; TODO We should be doing this... + str.count = length; return length; } // Returns true when the string is empty or consists of space characters. -// TODO Maybe rename to: is_empty -is_empty_string :: (str: string) -> bool { +is_empty :: (str: string) -> bool { for 0..str.count-1 { if str[it] == { case #char "\0"; #through; @@ -74,21 +77,21 @@ is_empty_string :: (str: string) -> bool { return true; } -// Counts number of characters in string. +// Counts the number of characters. count_characters :: (str: string) -> int { characters := 0; idx := 0; while idx < str.count { - idx += count_utf8_bytes(str[idx]); + idx += count_character_bytes(str[idx]); characters += 1; } return characters; } -// Delete character. +// Deletes character by it's index, and moves tail data to take its place. delete_character :: (str: *string, character_idx: int) { - buffer_idx := map_character_to_buffer_idx(str.*, character_idx); - bytes_to_delete := count_utf8_bytes(str.data[buffer_idx]); + buffer_idx := get_byte_idx(str.*, character_idx); + 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]; @@ -100,9 +103,8 @@ delete_character :: (str: *string, character_idx: int) { str.count -= bytes_to_delete; } -// Get character index. -// TODO Maybe rename to: map_character_to_byte_idx or get_character_byte_idx -map_character_to_buffer_idx :: (str: string, character_idx: int) -> buffer_idx: int, success: bool { +// Searches for the given character index and returns its byte index on the string. +get_byte_idx :: (str: string, character_idx: int) -> buffer_idx: int, success: bool { if character_idx < 0 then return -1, false; if character_idx > str.count then return -2, false; if character_idx == 0 then return 0, true; @@ -110,7 +112,7 @@ map_character_to_buffer_idx :: (str: string, character_idx: int) -> buffer_idx: buff_idx := 0; char_idx := 0; while buff_idx < str.count && char_idx != character_idx { - buff_idx += count_utf8_bytes(str[buff_idx]); + buff_idx += count_character_bytes(str[buff_idx]); char_idx += 1; } return buff_idx, char_idx == character_idx; diff --git a/ttt.jai b/ttt.jai index 3f7ac59..0802220 100644 --- a/ttt.jai +++ b/ttt.jai @@ -718,10 +718,9 @@ import_from_csv :: (db: *Database, path: string) -> bool { task: Task; csv_values := split(line, ",",, temporary_allocator); - // Import task name. - name_length := min(task.name.count, csv_values[0].count); - memcpy(task.name.data, csv_values[0].data, name_length); - truncate_string(xx task.name, name_length); + // Truncate and import task name. + truncate(*csv_values[0], task.name.count); + memcpy(task.name.data, csv_values[0].data, csv_values[0].count); advance(*csv_values); for csv_values @@ -1466,7 +1465,7 @@ main :: () { // 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_string(input) == false { + 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)); @@ -1516,7 +1515,7 @@ main :: () { input := read_input_string(input_pos_x, selected_task_row, input_width,, temporary_allocator); // Abort if input if empty. - if is_empty_string(input) continue; + if is_empty(input) continue; // Search for assign '=' operator and discard everything before it. assign_idx := find_index_from_left(input, "="); -- cgit v1.2.3 From 27e3e029448cf2a80b24e6e212dee8cccda06987 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 6 May 2024 13:05:12 +0100 Subject: WIP : Improve performance of draw_user_interface. --- ttt.jai | 18 ++++++++++++++++-- unused.jai | 19 ++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 0802220..d8233f9 100644 --- a/ttt.jai +++ b/ttt.jai @@ -895,7 +895,14 @@ update_layout :: () { } } -draw_tui :: (db: *Database, layout: *Layout) { +dbg_average := 0; // DEBUG +dbg_count := 0; // DEBUG +draw_user_interface :: (db: *Database, layout: *Layout) { + + start := current_time_monotonic(); // DEBUG + + + // TODO During dirty_flag revamp...we may also start using the String_Builder. @@ -1094,6 +1101,13 @@ draw_tui :: (db: *Database, layout: *Layout) { TUI.set_style(style_default); x += 1; print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); + + 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 + TUI.set_cursor_position(3, 1); + print("Average % us ---------", dbg_average/1000); // DEBUG } free_memory :: () { @@ -1371,7 +1385,7 @@ main :: () { write_strings(INVALID_WINDOW_MESSAGE); } else { - draw_tui(db, layout); + draw_user_interface(db, layout); draw_error_window(); } diff --git a/unused.jai b/unused.jai index ab4f667..d016cb7 100644 --- a/unused.jai +++ b/unused.jai @@ -3,14 +3,19 @@ print(">%<", S64_MIN + delta, to_standard_error = true); // MEASURE PERFORMANCE { - #import "Basic"; - start := current_time_monotonic(); - // Code to measure. - stop := current_time_monotonic(); - print("Measured % ns.\n", to_nanoseconds(stop-start)); + dbg_average := 0; // DEBUG + dbg_count := 0; // DEBUG - // Cumulative average: - CA_n+1 = (x_n+1 + n*CA_n ) / (n + 1) + #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. -- cgit v1.2.3 From 8779553b877a2bb728ddf9d7c21b272c2eedb653 Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 7 May 2024 10:27:22 +0100 Subject: Fixed biggest performance bottleneck on draw_user_interface: print white spaces. --- modules/TUI/module.jai | 2 +- modules/UTF8.jai | 40 ++++++++++++++++++++++++---------------- ttt.jai | 38 +++++++++++++++++++------------------- 3 files changed, 44 insertions(+), 36 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 7c3a71d..a5db3bf 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -572,7 +572,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { idx += 1; // Truncate string to avoid incomplete utf8 codes on the string tail. - truncate(*str, count_limit); + str.count = truncate(str, count_limit).count; } } diff --git a/modules/UTF8.jai b/modules/UTF8.jai index b583809..205e57d 100644 --- a/modules/UTF8.jai +++ b/modules/UTF8.jai @@ -21,15 +21,15 @@ count_character_bytes :: inline (leading_byte: u8) -> int { return 1; } -// Truncates the string to the provided length and zeroes the discarded bytes. -// Returns the length of truncated string or -1 if string has no data. -truncate :: (str: *string, length: int) -> length: int { - if str.data == null then return -1; +// Returns the string (using same str.data) truncated to the provided length. +truncate :: (str: string, length: int) -> string { - if str.count < length then length = str.count; + if str.data == null then return ""; + + if str.count < length then return .{str.count, str.data}; - data := str.data; - count := str.count; + data := str.data; + count := str.count; // Find index of first continuation byte. idx := length; @@ -50,12 +50,10 @@ truncate :: (str: *string, length: int) -> length: int { && !(continuation_bytes == 2 && (data[idx - 1] & 0xF0) == 0xE0) && !(continuation_bytes == 3 && (data[idx - 1] & 0xF8) == 0xF0) ) { - length -= (continuation_bytes + 1); // Remove start byte, ence '+ 1'. + length -= (continuation_bytes + 1); // Remove start byte, hence '+ 1'. } - - memset(data + length, 0, count - length); - str.count = length; - return length; + + return .{length, str.data}; } // Returns true when the string is empty or consists of space characters. @@ -78,13 +76,23 @@ is_empty :: (str: string) -> bool { } // Counts the number of characters. -count_characters :: (str: string) -> int { +count_characters :: (str: string, $is_null_terminated := false) -> int { characters := 0; idx := 0; - while idx < str.count { - idx += count_character_bytes(str[idx]); - characters += 1; + + #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; } diff --git a/ttt.jai b/ttt.jai index d8233f9..7d1559b 100644 --- a/ttt.jai +++ b/ttt.jai @@ -719,8 +719,8 @@ import_from_csv :: (db: *Database, path: string) -> bool { csv_values := split(line, ",",, temporary_allocator); // Truncate and import task name. - truncate(*csv_values[0], task.name.count); - memcpy(task.name.data, csv_values[0].data, csv_values[0].count); + 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 @@ -901,11 +901,11 @@ draw_user_interface :: (db: *Database, layout: *Layout) { start := current_time_monotonic(); // DEBUG - - - - // TODO During dirty_flag revamp...we may also start using the String_Builder. - + auto_release_temp(); + + empty_line := talloc_string(size_x); + memset(empty_line.data, #char " ", size_x); + adjust_first_day_of_week := int.[ (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, @@ -1004,6 +1004,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { idx_start := (db.selected_idx / layout_tasks_rows) * layout_tasks_rows; // Display up to rows allowed by the layout, or less if reached end of database. idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); + for task_idx: idx_start..idx_stop-1 { task := *db.tasks[task_idx]; y += 1; @@ -1026,19 +1027,18 @@ draw_user_interface :: (db: *Database, layout: *Layout) { // Task title. x += 1; column_width = layout.columns[L_TITLE_IDX].width; - // mvprintw(xx y, xx x, "%-*.*s", column_width, column_width, temp_c_string(xx task.name)); //task.name); TODO Fix required for LLVM/Cncurses. TODO DAM - // TODO FIXME OH MY GOD SUCH BAD CODEZ + // 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); - white_spaces := column_width; - // FIXME Improve by using buffer instead of printing one char at the time. - while white_spaces > 0 { - print_character(#char " "); - white_spaces -= 1; - } - TUI.set_cursor_position(x, y); - task_name: string = cast(string)task.name; - task_name.count = ifx task_name.count > column_width then column_width else task_name.count; - print("%", task_name); + 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. -- cgit v1.2.3 From 63c21762a8d8323facaf729a1bd17e9449eea72e Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 7 May 2024 16:54:23 +0100 Subject: WIP : Improve draw_user_interface by using buffer on the print/write calls. --- modules/TUI/module.jai | 122 +++++++++++++++++++++++++++++++++---------------- ttt.jai | 61 ++++++++++++++----------- 2 files changed, 116 insertions(+), 67 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index a5db3bf..f2a3a23 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -32,14 +32,17 @@ #import "UTF8"; #load "key_map.jai"; +#add_context tui_style : Style; // This contains the last style applied by the module. +#add_context tui_buffer : *String_Builder; // If set, this buffer will be used as output target of 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; - -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. +temp_buffer := String_Builder.{ allocator = temporary_allocator }; #scope_module @@ -51,7 +54,7 @@ assert_is_active :: inline () { log_tui_error :: (format_string: string, args: .. Any) { write_strings(Commands.SaveCursorPosition, Commands.MainScreenBuffer); - log_error(format_string, args); + log_error(format_string, ..args); write_strings(Commands.AlternateScreenBuffer, Commands.RestoreCursorPosition); } @@ -151,8 +154,6 @@ Commands :: struct #type_info_none { CursorNormalMode :: "\e[?1l"; } -#add_context terminal_style: Style; - Style :: struct { #if COLOR_MODE_BITS == 4 || COLOR_MODE_BITS == 8 { background: Palette; @@ -175,25 +176,26 @@ Style :: struct { } set_style :: (style: Style) { - auto_release_temp(); - builder := String_Builder.{ allocator = temporary_allocator }; + auto_release_temp(); + + builder := ifx context.tui_buffer != null then context.tui_buffer else *temp_buffer; #if COLOR_MODE_BITS == { case 4; - print_to_builder(*builder, + 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, + 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, + 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 @@ -201,25 +203,27 @@ set_style :: (style: Style) { } if style.use_default_foreground_color { - append(*builder, #run sprint(Commands.SetGraphicsRendition, "39")); + append(builder, #run sprint(Commands.SetGraphicsRendition, "39")); } if style.use_default_background_color { - append(*builder, #run sprint(Commands.SetGraphicsRendition, "49")); + append(builder, #run sprint(Commands.SetGraphicsRendition, "49")); } - write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + if context.tui_buffer == null { + write_builder(builder); + } - context.terminal_style = style; + context.tui_style = style; } clear_style :: () { write_string(#run sprint(Commands.SetGraphicsRendition, "0")); - context.terminal_style = .{ }; + context.tui_style = .{ }; } using_style :: (style: Style) #expand { - __style := context.terminal_style; + __style := context.tui_style; set_style(style); `defer set_style(__style); } @@ -493,7 +497,11 @@ 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."); - builder := String_Builder.{ allocator = temporary_allocator }; + // builder := String_Builder.{ allocator = temporary_allocator }; + // builder := String_Builder.{}; + // init_string_builder(*builder, 10000); + // reset(*builder); + builder := temp_buffer; str := alloc_string(count_limit); str.count = 0; @@ -509,7 +517,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { chars_count := count_characters(str); - // Draw preview. + // Preview input line. if is_visible { print_to_builder(*builder, Commands.SetCursorPosition, y, x); append(*builder, str); @@ -521,6 +529,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { for chars_count..count_limit-1 append(*builder, " "); } print_to_builder(*builder, Commands.SetCursorPosition, y, x+idx); + // write_builder(*builder); // TODO Not sure why this is not working... write_string(builder_to_string(*builder,, allocator = temporary_allocator)); // Process input key. @@ -594,38 +603,40 @@ draw_box :: (x: int, y: int, width: int, height: int) { 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."); auto_release_temp(); - - builder := String_Builder.{ allocator = temporary_allocator }; - - append(*builder, Commands.DrawingMode); + + builder := ifx context.tui_buffer != null then context.tui_buffer else *temp_buffer; + + append(builder, Commands.DrawingMode); // Draw top line - print_to_builder(*builder, Commands.SetCursorPosition, y, x); - append(*builder, Drawings.CornerTL); + print_to_builder(builder, Commands.SetCursorPosition, y, x); + append(builder, Drawings.CornerTL); for 1..width-2 { - append(*builder, Drawings.LineH); + append(builder, Drawings.LineH); } - append(*builder, Drawings.CornerTR); + 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); - print_to_builder(*builder, Commands.SetCursorPosition, idx, x+width-1); - append(*builder, Drawings.LineV); + print_to_builder(builder, Commands.SetCursorPosition, idx, x); + append(builder, Drawings.LineV); + 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); + 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.LineH); } - append(*builder, Drawings.CornerBR); + append(builder, Drawings.CornerBR); - append(*builder, Commands.TextMode); - - write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + append(builder, Commands.TextMode); + + if context.tui_buffer == null { + write_builder(builder); + } } clear_terminal :: inline () { @@ -682,7 +693,12 @@ get_terminal_size :: () -> width: int, height: int { set_cursor_position :: inline (x: int, y: int) { assert_is_active(); - print(Commands.SetCursorPosition, y, x); + if context.tui_buffer == null { + print(Commands.SetCursorPosition, y, x); + } + else { + print_to_builder(context.tui_buffer, Commands.SetCursorPosition, y, x); + } } get_cursor_position :: () -> x: int, y: int { @@ -723,3 +739,29 @@ set_terminal_title :: inline (title: string) { assert_is_active(); print(Commands.SetWindowTitle, title); } + +using_buffer :: (buffer: *String_Builder) #expand { + __buffer := context.tui_buffer; + context.tui_buffer = buffer; + `defer context.tui_buffer = __buffer; +} + +// TODO Maybe we should have a different name for this...? +tui_print :: inline (format_string: string, args: .. Any) { + if context.tui_buffer == null { + print(format_string, ..args, to_standard_error = false); + } + else { + print_to_builder(context.tui_buffer, format_string, ..args); + } +} + +// TODO Maybe we should have a different name for this...? +tui_write :: inline (format_string: string) { + if context.tui_buffer == null { + write_string(format_string); + } + else { + append(context.tui_buffer, format_string); + } +} diff --git a/ttt.jai b/ttt.jai index 7d1559b..ff3c757 100644 --- a/ttt.jai +++ b/ttt.jai @@ -216,29 +216,29 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { print_padding :: (size: int, char: u8 = #char " ") { assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience."); - while size > 0 { - print_character(char); - size -= 1; - } + auto_release_temp(); + padding := talloc_string(size); + memset(padding.data, char, size); + TUI.tui_write(padding); } TUI.set_cursor_position(x, y); if time < 0 { print_padding(left_padding); - write_string(" - "); + TUI.tui_write(" - "); print_padding(right_padding); return 0; } else if time == 0 { print_padding(left_padding); - write_string(" 0 "); + TUI.tui_write(" 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 " "}); + TUI.tui_print("%s ", FormatInt.{value = time, minimum_digits=3, padding=#char " "}); print_padding(right_padding); return 0; } @@ -246,7 +246,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { 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}); + TUI.tui_print("%:%", FormatInt.{value = hours, minimum_digits=2}, FormatInt.{value = minutes, minimum_digits=2}); print_padding(right_padding); return 0; } @@ -257,7 +257,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { 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}); + TUI.tui_print("%d", FormatFloat.{value = value, trailing_width=decimals, width=4}); print_padding(right_padding); return 0; } @@ -268,13 +268,13 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { 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}); + TUI.tui_print("%y", FormatFloat.{value = value, trailing_width=decimals, width=4}); print_padding(right_padding); return 0; } else { print_padding(left_padding); - write_string(" ∞ "); + TUI.tui_write(" ∞ "); print_padding(right_padding); return 0; } @@ -897,6 +897,7 @@ update_layout :: () { dbg_average := 0; // DEBUG dbg_count := 0; // DEBUG +buffer: String_Builder; // TODO draw_user_interface :: (db: *Database, layout: *Layout) { start := current_time_monotonic(); // DEBUG @@ -905,6 +906,11 @@ draw_user_interface :: (db: *Database, layout: *Layout) { empty_line := talloc_string(size_x); memset(empty_line.data, #char " ", size_x); + + init_string_builder(*buffer, 100000); + // builder := String_Builder.{ allocator = temporary_allocator }; + builder := buffer; + TUI.using_buffer(*builder); adjust_first_day_of_week := int.[ (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, @@ -931,25 +937,25 @@ draw_user_interface :: (db: *Database, layout: *Layout) { // Draw outer border. TUI.draw_box(1, 1, size_x, size_y); - + // Draw table grids. // TODO Maybe this could be simplified? y = 1; x = 1; - write_string(TUI.Commands.DrawingMode); + TUI.tui_write(TUI.Commands.DrawingMode); // append(*builder, TUI.Commands.DrawingMode); TODO 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); + TUI.tui_write(TUI.Drawings.TeeT); // TODO append(*builder, TUI.Drawings.TeeT); for row: 2..size_y { TUI.set_cursor_position(x, row); - write_string(TUI.Drawings.LineV); + TUI.tui_write(TUI.Drawings.LineV); // TODO append(*builder, TUI.Drawings.LineV); } TUI.set_cursor_position(x, size_y); - write_string(TUI.Drawings.TeeB); + TUI.tui_write(TUI.Drawings.TeeB); // TODO append(*builder, TUI.Drawings.TeeB); } - write_string(TUI.Commands.TextMode); + TUI.tui_write(TUI.Commands.TextMode); // TODO append(*builder, TUI.Commands.TextMode); /////////////////////////////////////////////////////////////////////////// @@ -961,7 +967,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { 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); + TUI.tui_write(ifx db == *archive then layout.archive_title else col.header); // TODO append(*builder, ifx db == *archive then layout.archive_title else col.header); x += col.width; // Headers : days @@ -981,7 +987,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { } col = *layout.columns[L_DAYS_IDX + idx]; TUI.set_cursor_position(x + col.alignment_offset, y); - write_string(col.header); + TUI.tui_write(col.header); // TODO append(*builder, col.header); x += col.width; } TUI.set_style(style_default); @@ -990,7 +996,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { x += 1; col = *layout.columns[L_TOTAL_IDX]; TUI.set_cursor_position(x + col.alignment_offset, y); - write_string(col.header); + TUI.tui_write(col.header); // TODO append(*builder, col.header); /////////////////////////////////////////////////////////////////////////// @@ -1033,11 +1039,11 @@ draw_user_interface :: (db: *Database, layout: *Layout) { task_name := cast(string)task.name; task_name = truncate(task_name, column_width); TUI.set_cursor_position(x, y); - write_string(task_name); + TUI.tui_write(task_name); // TODO append(*builder, 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); + TUI.tui_write(paint_remaining); // TODO append(*builder, paint_remaining); x += column_width; @@ -1065,10 +1071,10 @@ draw_user_interface :: (db: *Database, layout: *Layout) { 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); + TUI.tui_print(" %/% ", db.selected_idx + 1, db.tasks.count); } else { - print("%", db.selected_idx + 1); + TUI.tui_print("%", db.selected_idx + 1); } @@ -1107,7 +1113,9 @@ draw_user_interface :: (db: *Database, layout: *Layout) { dbg_average = (dbg_sample + dbg_count * dbg_average) / (dbg_count + 1); // DEBUG dbg_count += 1; // DEBUG TUI.set_cursor_position(3, 1); - print("Average % us ---------", dbg_average/1000); // DEBUG + TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, context.temporary_storage.total_bytes_occupied, context.temporary_storage.high_water_mark, context.temporary_storage.size); // DEBUG + // write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + write_builder(*builder); } free_memory :: () { @@ -1117,7 +1125,6 @@ free_memory :: () { free(app_directory); free(db_file_path); free(ar_file_path); - // reset_temporary_storage(); // TODO Not needed... I guess. } read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> value: string, success: bool { @@ -1133,7 +1140,7 @@ read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> val write_string(TUI.Commands.TextMode); TUI.set_cursor_position(x, y); - style_input := context.terminal_style; + style_input := context.tui_style; style_input.underline = true; TUI.using_style(style_input); -- cgit v1.2.3 From 89157d00fc4110055d5816cd1897e4def120bcaf Mon Sep 17 00:00:00 2001 From: dam Date: Tue, 7 May 2024 18:14:45 +0100 Subject: Fixed two very nasty bugs. --- modules/TUI/module.jai | 18 +++++++----------- ttt.jai | 2 ++ 2 files changed, 9 insertions(+), 11 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index f2a3a23..612f93e 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -177,8 +177,9 @@ Style :: struct { set_style :: (style: Style) { - auto_release_temp(); - + auto_release_temp(); // TODO Does this breaks if the context.tui_buffer is a temporary buffer... and we're writting to it? + + // TODO If context.tui_buffer is temporary... this will fail.... right? builder := ifx context.tui_buffer != null then context.tui_buffer else *temp_buffer; #if COLOR_MODE_BITS == { @@ -497,10 +498,6 @@ 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."); - // builder := String_Builder.{ allocator = temporary_allocator }; - // builder := String_Builder.{}; - // init_string_builder(*builder, 10000); - // reset(*builder); builder := temp_buffer; str := alloc_string(count_limit); @@ -529,8 +526,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { for chars_count..count_limit-1 append(*builder, " "); } print_to_builder(*builder, Commands.SetCursorPosition, y, x+idx); - // write_builder(*builder); // TODO Not sure why this is not working... - write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + write_builder(*builder); // Process input key. key = get_key(); @@ -560,7 +556,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { if idx == 0 continue; idx -= 1; delete_character(*str, idx); - + case; if is_escape_code(key) continue; @@ -571,7 +567,7 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { if buff_idx > count_limit - key_str.count then continue; // Move text to allow inserting new character. - for < count_limit..buff_idx+key_str.count { + for < count_limit-1..buff_idx + key_str.count-1 { str.data[it] = str.data[it-key_str.count]; } @@ -602,7 +598,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { 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."); - auto_release_temp(); + auto_release_temp(); // TODO Does this breaks if the context.tui_buffer is a temporary buffer... and we're writting to it? builder := ifx context.tui_buffer != null then context.tui_buffer else *temp_buffer; diff --git a/ttt.jai b/ttt.jai index ff3c757..612e6b6 100644 --- a/ttt.jai +++ b/ttt.jai @@ -218,6 +218,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience."); auto_release_temp(); padding := talloc_string(size); + padding.count = size; memset(padding.data, char, size); TUI.tui_write(padding); } @@ -1131,6 +1132,7 @@ read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> val // TODO Draw padding (at end of inputbox)... padding was renamed to input_width... is this the best name? // TODO Maybe add another optional arg with the placeholder text (to be preset on the input)? + // TODO COULD CACHE... but there's not need... TUI.set_cursor_position(x + input_limit, y); write_string(TUI.Commands.DrawingMode); -- cgit v1.2.3 From cfd38fe13acd19c9326264766cfe94e5534db739 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 8 May 2024 16:26:30 +0100 Subject: Added TODO entry. --- ttt.jai | 3 +++ 1 file changed, 3 insertions(+) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 612e6b6..2dbc5eb 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1194,6 +1194,9 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { main :: () { +// TODO WIP WIP WIP WIP WIP WIP WIP WIP - make some tests to see how auto_release_temporary works... check if something allocated right before are kept or discarddled... and how temp allocated StringBuilder behaves... + + #if DEBUG_MEMORY { defer report_memory_leaks(); // TODO Remove after final debug sessions. } -- cgit v1.2.3 From f5fc7f6e700d272c2d60e4a1da219c993f01cad3 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 9 May 2024 11:43:31 +0100 Subject: Fixed TUI module to allow returning results in temporary memory. --- modules/TUI/module.jai | 58 +++++++++++++++++++++++++++++--------------------- ttt.jai | 20 +++++++---------- unused.jai | 1 + 3 files changed, 43 insertions(+), 36 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 112c666..f8e745e 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -33,7 +33,7 @@ #load "key_map.jai"; #add_context tui_style : Style; // This contains the last style applied by the module. -#add_context tui_buffer : *String_Builder; // If set, this buffer will be used as output target of module procedures. +#add_context tui_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. @@ -42,7 +42,7 @@ active := false; input_override : Key; input_string : string; input_buffer : [1024] u8; -temp_buffer := String_Builder.{ allocator = temporary_allocator }; +temp_builder := String_Builder.{ allocator = temporary_allocator }; #scope_module @@ -176,11 +176,13 @@ Style :: struct { } set_style :: (style: Style) { - - auto_release_temp(); // TODO Does this breaks if the context.tui_buffer is a temporary buffer... and we're writting to it? - - // TODO If context.tui_buffer is temporary... this will fail.... right? - builder := ifx context.tui_buffer != null then context.tui_buffer else *temp_buffer; + // If no tui_builder is provided, use a temporary one and discard it afterwards. + builder := context.tui_builder; + temp_mark: Temporary_Storage_State = ---; + if context.tui_builder == null { + builder = *temp_builder; + temp_mark = get_temporary_storage_mark(); + } #if COLOR_MODE_BITS == { case 4; @@ -211,8 +213,9 @@ set_style :: (style: Style) { append(builder, #run sprint(Commands.SetGraphicsRendition, "49")); } - if context.tui_buffer == null { + if context.tui_builder == null { write_builder(builder); + set_temporary_storage_mark(temp_mark); } context.tui_style = style; @@ -497,9 +500,9 @@ read_input :: (count_limit: int = -1, terminators: .. u8) -> string { 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."); - - builder := temp_buffer; - + + // 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; @@ -510,6 +513,8 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { write_strings(Commands.ShowCursor, Commands.StartBlinking, Commands.BlinkingBarShape); while true { + builder := temp_builder; + auto_release_temp(); chars_count := count_characters(str); @@ -601,9 +606,13 @@ draw_box :: (x: int, y: int, width: int, height: int) { 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."); - auto_release_temp(); // TODO Does this breaks if the context.tui_buffer is a temporary buffer... and we're writting to it? - - builder := ifx context.tui_buffer != null then context.tui_buffer else *temp_buffer; + // If no tui_builder is provided, use a temporary one and discard it afterwards. + builder := context.tui_builder; + temp_mark: Temporary_Storage_State = ---; + if context.tui_builder == null { + builder = *temp_builder; + temp_mark = get_temporary_storage_mark(); + } append(builder, Commands.DrawingMode); @@ -633,8 +642,9 @@ draw_box :: (x: int, y: int, width: int, height: int) { append(builder, Commands.TextMode); - if context.tui_buffer == null { + if context.tui_builder == null { write_builder(builder); + set_temporary_storage_mark(temp_mark); } } @@ -692,11 +702,11 @@ get_terminal_size :: () -> width: int, height: int { set_cursor_position :: inline (x: int, y: int) { assert_is_active(); - if context.tui_buffer == null { + if context.tui_builder == null { print(Commands.SetCursorPosition, y, x); } else { - print_to_builder(context.tui_buffer, Commands.SetCursorPosition, y, x); + print_to_builder(context.tui_builder, Commands.SetCursorPosition, y, x); } } @@ -740,27 +750,27 @@ set_terminal_title :: inline (title: string) { } using_buffer :: (buffer: *String_Builder) #expand { - __buffer := context.tui_buffer; - context.tui_buffer = buffer; - `defer context.tui_buffer = __buffer; + __buffer := context.tui_builder; + context.tui_builder = buffer; + `defer context.tui_builder = __buffer; } // TODO Maybe we should have a different name for this...? tui_print :: inline (format_string: string, args: .. Any) { - if context.tui_buffer == null { + if context.tui_builder == null { print(format_string, ..args, to_standard_error = false); } else { - print_to_builder(context.tui_buffer, format_string, ..args); + print_to_builder(context.tui_builder, format_string, ..args); } } // TODO Maybe we should have a different name for this...? tui_write :: inline (format_string: string) { - if context.tui_buffer == null { + if context.tui_builder == null { write_string(format_string); } else { - append(context.tui_buffer, format_string); + append(context.tui_builder, format_string); } } diff --git a/ttt.jai b/ttt.jai index 2dbc5eb..efafb00 100644 --- a/ttt.jai +++ b/ttt.jai @@ -214,13 +214,12 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { left_padding := (space - TIME_CHARS) / 2; right_padding := space - TIME_CHARS - left_padding; - print_padding :: (size: int, char: u8 = #char " ") { + print_padding :: (size: int) { assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience."); - auto_release_temp(); - padding := talloc_string(size); - padding.count = size; - memset(padding.data, char, size); - TUI.tui_write(padding); + while size > 0 { + TUI.tui_write(" "); + size -= 1; + } } TUI.set_cursor_position(x, y); @@ -908,9 +907,9 @@ draw_user_interface :: (db: *Database, layout: *Layout) { empty_line := talloc_string(size_x); memset(empty_line.data, #char " ", size_x); - init_string_builder(*buffer, 100000); - // builder := String_Builder.{ allocator = temporary_allocator }; - builder := buffer; + // init_string_builder(*buffer, 100000); + // builder := buffer; + builder := String_Builder.{ allocator = temporary_allocator }; TUI.using_buffer(*builder); adjust_first_day_of_week := int.[ @@ -1194,9 +1193,6 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { main :: () { -// TODO WIP WIP WIP WIP WIP WIP WIP WIP - make some tests to see how auto_release_temporary works... check if something allocated right before are kept or discarddled... and how temp allocated StringBuilder behaves... - - #if DEBUG_MEMORY { defer report_memory_leaks(); // TODO Remove after final debug sessions. } diff --git a/unused.jai b/unused.jai index f9bb2f9..1f71b37 100644 --- a/unused.jai +++ b/unused.jai @@ -1,6 +1,7 @@ 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 -- cgit v1.2.3 From c0dc68c4672f53f2b9c60074bfb1eb10ab87d980 Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 9 May 2024 12:39:46 +0100 Subject: WIP : Renaming TUI print/write procedures. --- modules/TUI/module.jai | 6 +++--- ttt.jai | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index f8e745e..5399503 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -766,11 +766,11 @@ tui_print :: inline (format_string: string, args: .. Any) { } // TODO Maybe we should have a different name for this...? -tui_write :: inline (format_string: string) { +tui_write_string :: inline (s: string) { if context.tui_builder == null { - write_string(format_string); + write_string(s, to_standard_error = false); } else { - append(context.tui_builder, format_string); + append(context.tui_builder, s); } } diff --git a/ttt.jai b/ttt.jai index efafb00..e5ade13 100644 --- a/ttt.jai +++ b/ttt.jai @@ -217,7 +217,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { print_padding :: (size: int) { assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience."); while size > 0 { - TUI.tui_write(" "); + TUI.tui_write_string(" "); size -= 1; } } @@ -226,13 +226,13 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { if time < 0 { print_padding(left_padding); - TUI.tui_write(" - "); + TUI.tui_write_string(" - "); print_padding(right_padding); return 0; } else if time == 0 { print_padding(left_padding); - TUI.tui_write(" 0 "); + TUI.tui_write_string(" 0 "); print_padding(right_padding); return 0; } @@ -274,7 +274,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { } else { print_padding(left_padding); - TUI.tui_write(" ∞ "); + TUI.tui_write_string(" ∞ "); print_padding(right_padding); return 0; } @@ -942,20 +942,20 @@ draw_user_interface :: (db: *Database, layout: *Layout) { // TODO Maybe this could be simplified? y = 1; x = 1; - TUI.tui_write(TUI.Commands.DrawingMode); // append(*builder, TUI.Commands.DrawingMode); TODO + TUI.tui_write_string(TUI.Commands.DrawingMode); // append(*builder, TUI.Commands.DrawingMode); TODO for 0..layout.columns.count-2 { column := layout.columns[it]; x += 1 + column.width; TUI.set_cursor_position(x, y); - TUI.tui_write(TUI.Drawings.TeeT); // TODO append(*builder, TUI.Drawings.TeeT); + TUI.tui_write_string(TUI.Drawings.TeeT); // TODO append(*builder, TUI.Drawings.TeeT); for row: 2..size_y { TUI.set_cursor_position(x, row); - TUI.tui_write(TUI.Drawings.LineV); // TODO append(*builder, TUI.Drawings.LineV); + TUI.tui_write_string(TUI.Drawings.LineV); // TODO append(*builder, TUI.Drawings.LineV); } TUI.set_cursor_position(x, size_y); - TUI.tui_write(TUI.Drawings.TeeB); // TODO append(*builder, TUI.Drawings.TeeB); + TUI.tui_write_string(TUI.Drawings.TeeB); // TODO append(*builder, TUI.Drawings.TeeB); } - TUI.tui_write(TUI.Commands.TextMode); // TODO append(*builder, TUI.Commands.TextMode); + TUI.tui_write_string(TUI.Commands.TextMode); // TODO append(*builder, TUI.Commands.TextMode); /////////////////////////////////////////////////////////////////////////// @@ -967,7 +967,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { x += 1; col = *layout.columns[L_TITLE_IDX]; TUI.set_cursor_position(x + col.alignment_offset, y); - TUI.tui_write(ifx db == *archive then layout.archive_title else col.header); // TODO append(*builder, ifx db == *archive then layout.archive_title else col.header); + TUI.tui_write_string(ifx db == *archive then layout.archive_title else col.header); // TODO append(*builder, ifx db == *archive then layout.archive_title else col.header); x += col.width; // Headers : days @@ -987,7 +987,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { } col = *layout.columns[L_DAYS_IDX + idx]; TUI.set_cursor_position(x + col.alignment_offset, y); - TUI.tui_write(col.header); // TODO append(*builder, col.header); + TUI.tui_write_string(col.header); // TODO append(*builder, col.header); x += col.width; } TUI.set_style(style_default); @@ -996,7 +996,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { x += 1; col = *layout.columns[L_TOTAL_IDX]; TUI.set_cursor_position(x + col.alignment_offset, y); - TUI.tui_write(col.header); // TODO append(*builder, col.header); + TUI.tui_write_string(col.header); // TODO append(*builder, col.header); /////////////////////////////////////////////////////////////////////////// @@ -1039,11 +1039,11 @@ draw_user_interface :: (db: *Database, layout: *Layout) { task_name := cast(string)task.name; task_name = truncate(task_name, column_width); TUI.set_cursor_position(x, y); - TUI.tui_write(task_name); // TODO append(*builder, task_name); + TUI.tui_write_string(task_name); // TODO append(*builder, 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 }; - TUI.tui_write(paint_remaining); // TODO append(*builder, paint_remaining); + TUI.tui_write_string(paint_remaining); // TODO append(*builder, paint_remaining); x += column_width; -- cgit v1.2.3 From 18cd73bb1830fd49089eb82667ba6ae14606938c Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 9 May 2024 22:24:16 +0100 Subject: Renamed stuff to make TUI output builder purpose more clear. --- modules/TUI/module.jai | 40 +++++++++++++++++++--------------------- ttt.jai | 29 ++++++++++++++++++++--------- 2 files changed, 39 insertions(+), 30 deletions(-) (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index 5399503..70675c7 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -33,7 +33,7 @@ #load "key_map.jai"; #add_context tui_style : Style; // This contains the last style applied by the module. -#add_context tui_builder : *String_Builder; // If set, this will serve as an output buffer for this module procedures. +#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. @@ -176,10 +176,10 @@ Style :: struct { } set_style :: (style: Style) { - // If no tui_builder is provided, use a temporary one and discard it afterwards. - builder := context.tui_builder; + // 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_builder == null { + if context.tui_output_builder == null { builder = *temp_builder; temp_mark = get_temporary_storage_mark(); } @@ -213,7 +213,7 @@ set_style :: (style: Style) { append(builder, #run sprint(Commands.SetGraphicsRendition, "49")); } - if context.tui_builder == null { + if context.tui_output_builder == null { write_builder(builder); set_temporary_storage_mark(temp_mark); } @@ -606,10 +606,10 @@ draw_box :: (x: int, y: int, width: int, height: int) { 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_builder is provided, use a temporary one and discard it afterwards. - builder := context.tui_builder; + // 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_builder == null { + if context.tui_output_builder == null { builder = *temp_builder; temp_mark = get_temporary_storage_mark(); } @@ -642,7 +642,7 @@ draw_box :: (x: int, y: int, width: int, height: int) { append(builder, Commands.TextMode); - if context.tui_builder == null { + if context.tui_output_builder == null { write_builder(builder); set_temporary_storage_mark(temp_mark); } @@ -702,11 +702,11 @@ get_terminal_size :: () -> width: int, height: int { set_cursor_position :: inline (x: int, y: int) { assert_is_active(); - if context.tui_builder == null { + if context.tui_output_builder == null { print(Commands.SetCursorPosition, y, x); } else { - print_to_builder(context.tui_builder, Commands.SetCursorPosition, y, x); + print_to_builder(context.tui_output_builder, Commands.SetCursorPosition, y, x); } } @@ -749,28 +749,26 @@ set_terminal_title :: inline (title: string) { print(Commands.SetWindowTitle, title); } -using_buffer :: (buffer: *String_Builder) #expand { - __buffer := context.tui_builder; - context.tui_builder = buffer; - `defer context.tui_builder = __buffer; +using_builder_as_output :: (builder: *String_Builder) #expand { + __builder := context.tui_output_builder; + context.tui_output_builder = builder; + `defer context.tui_output_builder = __builder; } -// TODO Maybe we should have a different name for this...? tui_print :: inline (format_string: string, args: .. Any) { - if context.tui_builder == null { + if context.tui_output_builder == null { print(format_string, ..args, to_standard_error = false); } else { - print_to_builder(context.tui_builder, format_string, ..args); + print_to_builder(context.tui_output_builder, format_string, ..args); } } -// TODO Maybe we should have a different name for this...? tui_write_string :: inline (s: string) { - if context.tui_builder == null { + if context.tui_output_builder == null { write_string(s, to_standard_error = false); } else { - append(context.tui_builder, s); + append(context.tui_output_builder, s); } } diff --git a/ttt.jai b/ttt.jai index e5ade13..01803b2 100644 --- a/ttt.jai +++ b/ttt.jai @@ -204,6 +204,11 @@ count_digits :: (number: s64, base: s64 = 10) -> s64 { // 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); @@ -217,7 +222,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { print_padding :: (size: int) { assert(size >= 0, "Cannot print negative padding values. The procedure accepts signed values just for convenience."); while size > 0 { - TUI.tui_write_string(" "); + write_string(" "); size -= 1; } } @@ -226,19 +231,19 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { if time < 0 { print_padding(left_padding); - TUI.tui_write_string(" - "); + write_string(" - "); print_padding(right_padding); return 0; } else if time == 0 { print_padding(left_padding); - TUI.tui_write_string(" 0 "); + write_string(" 0 "); print_padding(right_padding); return 0; } else if time < SECONDS_IN_MINUTE { print_padding(left_padding); - TUI.tui_print("%s ", FormatInt.{value = time, minimum_digits=3, padding=#char " "}); + print("%s ", FormatInt.{value = time, minimum_digits=3, padding=#char " "}); print_padding(right_padding); return 0; } @@ -246,7 +251,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { hours := time / SECONDS_IN_HOUR; minutes := (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE; print_padding(left_padding); - TUI.tui_print("%:%", FormatInt.{value = hours, minimum_digits=2}, FormatInt.{value = minutes, minimum_digits=2}); + print("%:%", FormatInt.{value = hours, minimum_digits=2}, FormatInt.{value = minutes, minimum_digits=2}); print_padding(right_padding); return 0; } @@ -257,7 +262,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { ifx time >= #run mul_f64_s64(9.995, SECONDS_IN_DAY) then 1 else 2; print_padding(left_padding); - TUI.tui_print("%d", FormatFloat.{value = value, trailing_width=decimals, width=4}); + print("%d", FormatFloat.{value = value, trailing_width=decimals, width=4}); print_padding(right_padding); return 0; } @@ -268,13 +273,13 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { ifx time >= #run mul_f64_s64(9.995, SECONDS_IN_YEAR) then 1 else 2; print_padding(left_padding); - TUI.tui_print("%y", FormatFloat.{value = value, trailing_width=decimals, width=4}); + print("%y", FormatFloat.{value = value, trailing_width=decimals, width=4}); print_padding(right_padding); return 0; } else { print_padding(left_padding); - TUI.tui_write_string(" ∞ "); + write_string(" ∞ "); print_padding(right_padding); return 0; } @@ -907,10 +912,16 @@ draw_user_interface :: (db: *Database, layout: *Layout) { empty_line := talloc_string(size_x); memset(empty_line.data, #char " ", size_x); + /* TODO + It's not safe to use temporary memory here because the console resolution may increase and use more than what we have in temporary memory. + And temporary memory is configured at compile time. + We should dynamically allocate memory with some headroom and, at beggining of function... adjust it if necessary. + // init_string_builder(*buffer, 100000); // builder := buffer; + */ builder := String_Builder.{ allocator = temporary_allocator }; - TUI.using_buffer(*builder); + TUI.using_builder_as_output(*builder); adjust_first_day_of_week := int.[ (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, -- cgit v1.2.3 From 3fd7b092ec6e2fa56603d35768f98b2ceeb9bfcc Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 9 May 2024 23:44:51 +0100 Subject: Added possible solution for active/selected task pointers potential issue. --- ttt.jai | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 01803b2..08d4bbd 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1413,8 +1413,11 @@ main :: () { update_times(*database); //timeout(INPUT_AWAIT_INF); TODO DAM - // TODO WIP Remove `selected_task` and `active_task` and helper functions. - // TODO Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. + /* TODO + Remove `selected_task` and `active_task` and helper functions. + Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. + Maybe use a macro returns the selected/active task based on the indices. + */ selected_task := get_selected_task(db); active_task := get_active_task(db); selected_task_row: int; -- cgit v1.2.3 From 98d89dd5cb622805c48bc065f8ac27bd0484a359 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 10 May 2024 15:05:43 +0100 Subject: Reduced draw calls when no input is happening. --- ttt.jai | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 110 insertions(+), 31 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 08d4bbd..acc1599 100644 --- a/ttt.jai +++ b/ttt.jai @@ -903,15 +903,35 @@ update_layout :: () { dbg_average := 0; // DEBUG dbg_count := 0; // DEBUG buffer: String_Builder; // TODO -draw_user_interface :: (db: *Database, layout: *Layout) { - - start := current_time_monotonic(); // DEBUG +draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) { + // 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) % 7; + } + + // 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 + 7) % 7; + } + + auto_release_temp(); - empty_line := talloc_string(size_x); - memset(empty_line.data, #char " ", size_x); - /* TODO It's not safe to use temporary memory here because the console resolution may increase and use more than what we have in temporary memory. And temporary memory is configured at compile time. @@ -932,17 +952,66 @@ draw_user_interface :: (db: *Database, layout: *Layout) { (5 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, (6 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, ]; - - x: int; - y: int; - col: *Column; // 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(*builder); + + return; + } + + x: int; + y: int; + col: *Column; + // Reset theme and clear screen. TUI.clear_terminal(); @@ -1015,14 +1084,12 @@ draw_user_interface :: (db: *Database, layout: *Layout) { total_time := 0; column_width: int; + + empty_line := talloc_string(size_x); + memset(empty_line.data, #char " ", size_x); y = 1; - // Pagination based on currently selected task (show page where selected task is). - idx_start := (db.selected_idx / layout_tasks_rows) * layout_tasks_rows; - // Display up to rows allowed by the layout, or less if reached end of database. - idx_stop := idx_start + (ifx layout_tasks_rows > db.tasks.count - idx_start then db.tasks.count - idx_start else layout_tasks_rows); - - for task_idx: idx_start..idx_stop-1 { + for task_idx: start_idx..stop_idx-1 { task := *db.tasks[task_idx]; y += 1; x = 1; @@ -1118,14 +1185,7 @@ draw_user_interface :: (db: *Database, layout: *Layout) { TUI.set_style(style_default); x += 1; print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); - - 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 - TUI.set_cursor_position(3, 1); - TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, context.temporary_storage.total_bytes_occupied, context.temporary_storage.high_water_mark, context.temporary_storage.size); // DEBUG - // write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + write_builder(*builder); } @@ -1384,11 +1444,10 @@ main :: () { initialize_tui(); - db := *database; - layout := *layouts[Layouts.COMPACT]; - - action_style: TUI.Style; - + db := *database; + layout := *layouts[Layouts.COMPACT]; + redraw_all := true; + action_style : TUI.Style; TUI.flush_input(); TUI.set_next_key(TUI.Keys.Resize); @@ -1404,15 +1463,35 @@ main :: () { write_strings(INVALID_WINDOW_MESSAGE); } else { - draw_user_interface(db, layout); +start := current_time_monotonic(); // DEBUG + + draw_user_interface(db, layout, redraw_all); + +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 +TUI.set_cursor_position(3, 1); +TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, context.temporary_storage.total_bytes_occupied, context.temporary_storage.high_water_mark, context.temporary_storage.size); // DEBUG + // write_string(builder_to_string(*builder,, allocator = temporary_allocator)); + draw_error_window(); + TUI.set_cursor_position(40, 1); + TUI.tui_print(">%<", redraw_all); } key := TUI.get_key(INPUT_TIMEOUT_MS); + if key == #char "q" || key == #char "Q" break; + + redraw_all = key != TUI.Keys.None; + update_times(*database); - //timeout(INPUT_AWAIT_INF); TODO DAM + + if key == #char "k" { dbg_average = 0; dbg_count = 0; redraw_all = false; continue; } // DEBUG + + /* TODO Remove `selected_task` and `active_task` and helper functions. Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. -- cgit v1.2.3 From 3dfabf220d80ae0329b828a848714e8e1ff7f291 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 10 May 2024 15:12:11 +0100 Subject: Replaced adjust_first_day_of_week table to be more readable. --- ttt.jai | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index acc1599..23d370c 100644 --- a/ttt.jai +++ b/ttt.jai @@ -943,16 +943,6 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) builder := String_Builder.{ allocator = temporary_allocator }; TUI.using_builder_as_output(*builder); - adjust_first_day_of_week := int.[ - (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - (2 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - (3 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - (4 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - (5 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - (6 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS, - ]; - // Get context information. active_task := get_active_task(db); selected_task := get_selected_task(db); @@ -1052,20 +1042,20 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) // Headers : days for 0..NUM_WEEK_DAYS-1 { - idx := adjust_first_day_of_week[it]; + day_idx := get_day_index_from_layout_index(it); x += 1; // Apply theme. - if (idx == now_week_day && active_task != null) { + if (day_idx == now_week_day && active_task != null) { TUI.set_style(style_active); } - else if (idx == now_week_day) { + 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 + idx]; + col = *layout.columns[L_DAYS_IDX + day_idx]; TUI.set_cursor_position(x + col.alignment_offset, y); TUI.tui_write_string(col.header); // TODO append(*builder, col.header); x += col.width; @@ -1162,22 +1152,22 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) x = 1 + 1 + layout.columns[L_TITLE_IDX].width; total_time = 0; for 0..NUM_WEEK_DAYS-1 { - idx := adjust_first_day_of_week[it]; - daily_total := db.total_times[idx]; + day_idx := get_day_index_from_layout_index(it); + daily_total := db.total_times[day_idx]; x += 1; // Apply theme. - if (idx == now_week_day && active_task != null) { + if (day_idx == now_week_day && active_task != null) { TUI.set_style(style_active); } - else if (idx == now_week_day) { + 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 + idx].width; + 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; -- cgit v1.2.3 From af6221d2b3d6d9f524959ffd15766a171f3ad798 Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 10 May 2024 15:55:36 +0100 Subject: Made start day of week configurable via input arg. Fixed display of time with decimals. --- ttt.jai | 125 ++++++++++++++++++++++++++++++++-------------------------------- 1 file changed, 63 insertions(+), 62 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 23d370c..33d5282 100644 --- a/ttt.jai +++ b/ttt.jai @@ -33,13 +33,9 @@ DEBUG_MEMORY :: true; #import "UTF8"; TUI :: #import "TUI"(COLOR_MODE_BITS=4); -// - fix/implement/finish TODO : use `dirty_bit_flag` to only update ehat has been changed - VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; -FIRST_DAY_OF_WEEK :: 1; // (0-6, Sunday = 0). -NUM_WEEK_DAYS :: 7; // TODO This has to go - Just to be more clear about what we're looping about. -NAME_SIZE :: 72; // TODO Use this instead of Task.name.count ? +NUM_WEEK_DAYS :: 7; APP_FOLDER_NAME :: ".task_time_tracker_test"; // TODO Using different folder to avoid erasing my work data. DB_FILE_NAME :: "database.bin"; @@ -74,6 +70,7 @@ 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; @@ -262,7 +259,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { 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}); + print("%d", FormatFloat.{value = value, trailing_width=decimals, width=4, zero_removal=.NO}); print_padding(right_padding); return 0; } @@ -273,7 +270,7 @@ print_time :: (y: int, x: int, time: s64, space: int) -> int { 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}); + print("%y", FormatFloat.{value = value, trailing_width=decimals, width=4, zero_removal=.NO}); print_padding(right_padding); return 0; } @@ -318,7 +315,7 @@ add_task :: (db: *Database, task: *Task = null) -> task: *Task, index: s64 { // Deletes task from database. // If possible, shrinks the database capacity. // Returns success. -delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `using db`. +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); @@ -361,40 +358,40 @@ delete_task :: (using db: *Database, index: s64) -> bool { // TODO Maybe use `us // Moves task from source to target. // Source and target get clamped to database size. -move_task :: (db: *Database, source: s64, target: s64) { // TODO Maybe `using db` +move_task :: (using db: *Database, source: s64, target: s64) { assert(db != null, ASSERT_NOT_NULL, "db"); - source = clamp(source, 0, db.tasks.count-1); - target = clamp(target, 0, db.tasks.count-1); + 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 := db.tasks[source]; + temp_task := tasks[source]; move_size := abs(target - source); if target > source { for 0..move_size-1 - db.tasks[source + it] = db.tasks[source + it + 1]; + tasks[source + it] = tasks[source + it + 1]; } else { for < move_size-1..0 - db.tasks[target + it + 1] = db.tasks[target + it]; + tasks[target + it + 1] = tasks[target + it]; } - db.tasks[target] = temp_task; + tasks[target] = temp_task; // Adjust active and selected tasks. - if (db.active_idx == source) { - db.active_idx = target; + if (active_idx == source) { + active_idx = target; } - else if (source < db.active_idx && db.active_idx <= target) { - db.active_idx -= 1; + else if (source < active_idx && active_idx <= target) { + active_idx -= 1; } - else if (target <= db.active_idx && db.active_idx < source) { - db.active_idx += 1; + else if (target <= active_idx && active_idx < source) { + active_idx += 1; } - db.selected_idx = target; + selected_idx = target; } // Find similar task and return it's index, or -1 if not found. @@ -506,7 +503,7 @@ add_task_time :: (db: *Database, index: s64, day: int, time: s64) { } // Adds the time on the day and task provided (and adjusts database totals). -add_task_times :: (db: *Database, index: s64, times: [7] s64) { +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); @@ -900,35 +897,36 @@ update_layout :: () { } } +// 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; +} + dbg_average := 0; // DEBUG dbg_count := 0; // DEBUG buffer: String_Builder; // TODO draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) { - // 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) % 7; - } - - // 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 + 7) % 7; - } - + auto_release_temp(); @@ -1119,7 +1117,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) total_time = 0; for 0..NUM_WEEK_DAYS-1 { x += 1; - day_idx := (it + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + 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); @@ -1188,15 +1186,10 @@ free_memory :: () { free(ar_file_path); } -read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> value: string, success: bool { - - // TODO Draw padding (at end of inputbox)... padding was renamed to input_width... is this the best name? - // TODO Maybe add another optional arg with the placeholder text (to be preset on the input)? - // TODO COULD CACHE... but there's not need... - +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..padding { + for 1..input_width { write_string(TUI.Drawings.Checkerboard); } write_string(TUI.Commands.TextMode); @@ -1208,7 +1201,6 @@ read_input_string :: (x: int, y: int, input_limit: int, padding: int = 0) -> val value, key := TUI.read_input_line(input_limit); return value, key == TUI.Keys.Enter; - } // Returns success. @@ -1312,6 +1304,7 @@ main :: () { "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", @@ -1411,6 +1404,16 @@ main :: () { is_exit_requested = true; continue; } + + if is_equal_to_any(args[it], "--start-of-week", "-s") { + it += 1; + if it >= args.count { + print_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; @@ -1422,8 +1425,7 @@ main :: () { } if is_exit_requested { - // exit(0); // TODO fucking exit does not report memory leaks. - return; + exit(0); } } @@ -1649,7 +1651,7 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont // Apply changes. time := cast(s64)input_time; - day := (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + 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); @@ -1765,7 +1767,6 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont // Sort by. case #char "s"; #through; case #char "S"; - // TODO The initial part should only decide what's the sorting procedure... then we would would all in a single place. 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(); @@ -1789,8 +1790,8 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont case #char "5"; #through; case #char "6"; #through; case #char "7"; - sort_by_idx := sort_by - #char "1"; - day := (sort_by_idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + 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]; -- cgit v1.2.3 From 739280d6aac054086219f2ee7e8b93ec62a76e8b Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 10 May 2024 16:06:40 +0100 Subject: Fixed styles to work better both on white and dark themes. --- ttt.jai | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 33d5282..f0d22be 100644 --- a/ttt.jai +++ b/ttt.jai @@ -83,7 +83,8 @@ pos_y : int; style_default := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.WHITE, - // use_default_background_color = true, TODO + use_default_background_color = true, + use_default_foreground_color = true, }; style_selected := TUI.Style.{ @@ -95,14 +96,14 @@ style_selected_inverted := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.CYAN, bold = true, - // use_default_background_color = true, TODO + use_default_background_color = true, }; style_active := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.BLUE, bold = true, - // use_default_background_color = true, TODO + use_default_background_color = true, }; style_active_selected := TUI.Style.{ @@ -115,7 +116,7 @@ style_error := TUI.Style.{ background = TUI.Palette.BLACK, foreground = TUI.Palette.RED, bold = true, - // use_default_background_color = true, TODO + use_default_background_color = true, }; Layouts :: enum u8 { -- cgit v1.2.3 From 5c3b4448575a15b5fed46071192ddc9736cda298 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 03:15:40 +0100 Subject: Fixed error messages and how they're displayed. --- ttt.jai | 220 +++++++++++++++++++++++++++++++++------------------------------- 1 file changed, 113 insertions(+), 107 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index f0d22be..7fff02d 100644 --- a/ttt.jai +++ b/ttt.jai @@ -125,23 +125,24 @@ Layouts :: enum u8 { } -error_message: string; +error_string_builder: String_Builder; error_time_limit := Apollo_Time.{0, 0}; -print_error :: (format :string, args : .. Any) { +print_error :: (message: string, data: *void, info: Log_Info) { if TUI.is_active() == false { - print(format, ..args, to_standard_error = true); - print("\n"); + write_strings(message, "\n", to_standard_error = true); return; } - if error_message.data != null { - free(error_message.data); + if error_time_limit < current_time_monotonic() { + reset(*error_string_builder); } - error_message = sprint(format, args); - + else { + append(*error_string_builder, " | "); + } + append(*error_string_builder, message); error_time_limit = current_time_monotonic() + seconds_to_apollo(5); } @@ -170,7 +171,7 @@ draw_error_window :: () { print_character(#char " "); } TUI.set_cursor_position(pos_x + 1, pos_y + 1); - write_string(error_message); + write_builder(*error_string_builder, false); } trigger_autosave :: () { @@ -183,6 +184,14 @@ show_processing :: () { 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; @@ -306,7 +315,7 @@ add_task :: (db: *Database, task: *Task = null) -> task: *Task, index: s64 { array_add(*db.tasks, new_task); for * db.total_times { - < idx: s64 { +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; @@ -408,7 +417,7 @@ find_similar_task :: (db: *Database, task: Task) -> idx: s64 { } for db.tasks { - if compare(xx task.name, xx it.name) == 0 && compare_array(task.times, it.times) == 0 { + if compare(xx task.name, xx it.name) == 0 && (ignore_times || compare_array(task.times, it.times) == 0) { return it_index; } } @@ -526,15 +535,12 @@ reset_database :: (db: *Database) { // Stores data from database into binary file. // Returns success. -store_database :: (db: Database, path: string) -> success: bool { +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); // log_errors: bool = true - if open_success == false { - print_error("Failed to open file '%' while storing database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! - return false; - } + 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); @@ -546,35 +552,30 @@ store_database :: (db: Database, path: string) -> success: bool { // Loads data from binary file into database. // Returns success. -load_database :: (db: *Database, path: string) -> success: bool { +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); // log_errors: bool = true - if open_success == false { - print_error("Failed to open file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! - return false; - } + 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 print_error("Failed to read file signature from '%'.", path); + if read_success == false log_error("Failed to read file signature."); if cast(string)file_signature != DB_FILE_SIGN_STR { - print_error("Invalid file signature."); + log_error("Invalid file signature while loading database.\n"); return false; } // Read database structure. read_success = file_read(file, db, size_of(Database)); - // TODO Use print_error or assert? if read_success == false { - print_error("Failed to read database info from '%'.", path); + log_error("Failed to read database info."); return false; } - assert(read_success == true, "Failed to read database info from '%'.", path); // Reserve database capacity for tasks. tasks_count := db.tasks.count; @@ -588,14 +589,20 @@ load_database :: (db: *Database, path: string) -> success: bool { // Make sure we are reading all the file. buffer: u8; success, bytes := file_read(file, *buffer, 1); - assert(bytes == 0, "Unexpected content found at the end of file '%'.", path); + if bytes > 0 { + log_error("Unexpected content found at the end of file '%'.", path); + return false; + } + + // Make sure we have a valid selected index. + if db.tasks.count > 0 && db.selected_idx < 0 then db.selected_idx = 0; return true; } // Exports data into CSV file. // Returns success. -export_to_csv :: (db: Database, path: string) -> success: bool { +export_to_csv :: (db: Database, path: string) -> success: bool #must { assert(xx path, ASSERT_NOT_EMPTY, "path"); auto_release_temp(); @@ -616,14 +623,12 @@ export_to_csv :: (db: Database, path: string) -> success: bool { name, it.times[0], it.times[1], it.times[2], it.times[3], it.times[4], it.times[5], it.times[6]); } - write_entire_file(path, *builder); - - return true; + return write_entire_file(path, *builder); } // Imports CSV file into database. // Returns success. -import_from_csv :: (db: *Database, path: string) -> bool { +import_from_csv :: (db: *Database, path: string) -> bool #must { // TODO Review code. assert(db != null, ASSERT_NOT_NULL, "db"); assert(xx path, ASSERT_NOT_EMPTY, "path"); @@ -649,7 +654,7 @@ import_from_csv :: (db: *Database, path: string) -> bool { csv := data; if success == false { - print_error("Failed to read file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! + log_error("Failed to read file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! return false; } @@ -746,15 +751,14 @@ import_from_csv :: (db: *Database, path: string) -> bool { // Appends task to the end of the CSV file. // Returns success. -append_to_csv :: (task: Task, path: string) -> success: bool { +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); - if file_success == false { - //print_error("Failed to open file '%s' while appending to CSV: %s.", path, strerror(errno)); // TODO Show internal error or something - return false; - } file_size := file_length(file); file_set_position(file, file_size-1); @@ -763,14 +767,13 @@ append_to_csv :: (task: Task, path: string) -> success: bool { if (last_char != #char "\n") { file_write(*file, "\n"); } - - task_name := copy_temporary_string(xx task.name); // TODO Cleanup this temp mess. + + 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]); - file_write(*file, csv_line); - - return true; + + return file_write(*file, csv_line); } // Selects task by index. @@ -832,7 +835,7 @@ layout_tasks_rows : int; is_terminal_too_small := true; -initialize_tui :: () { +initialize_user_interface :: () { // Normal layout. layouts[Layouts.NORMAL] = .{ @@ -881,7 +884,7 @@ initialize_tui :: () { col.alignment_offset = offset; } } - + assert(TUI.setup_terminal(), "Failed to setup TUI."); } @@ -926,8 +929,6 @@ dbg_average := 0; // DEBUG dbg_count := 0; // DEBUG buffer: String_Builder; // TODO draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) { - - auto_release_temp(); @@ -953,6 +954,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) // 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); @@ -1250,6 +1252,8 @@ main :: () { #if DEBUG_MEMORY { defer report_memory_leaks(); // TODO Remove after final debug sessions. } + + context.logger = print_error; defer free_memory(); @@ -1262,17 +1266,13 @@ main :: () { home_path, success_path := get_absolute_path(home_dir); // Returns temporary memory. if success_path == false { - print_error("Failed to find home directory '%'.", home_dir); + 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); - - // 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); } @@ -1280,14 +1280,14 @@ main :: () { { // Initialize database and archive files if needed. if (file_exists(db_file_path) == false) { if (store_database(database, db_file_path) == false) { - print_error("Failed to initialize database."); + log_error("Failed to initialize database."); exit(1); } } if (file_exists(ar_file_path) == false) { if (export_to_csv(archive, ar_file_path) == false) { - print_error("Failed to initialize archive."); + log_error("Failed to initialize archive."); exit(1); } } @@ -1367,19 +1367,19 @@ main :: () { if is_equal_to_any(args[it], "--import-csv", "-i") { it += 1; if it >= args.count { - print_error("Missing CSV file path to import."); + log_error("Missing CSV file path to import."); exit(1); } if (load_database(*database, db_file_path) == false) { - print_error("Failed to load database."); + log_error("Failed to load database during import."); exit(1); } if (import_from_csv(*database, args[it]) == false) { - print_error("Failed to import CSV file."); + log_error("Failed to import CSV file."); exit(1); } if (store_database(*database, db_file_path) == false) { - print_error("Failed to store database."); + log_error("Failed to store database during import."); exit(1); } reset_database(*database); @@ -1390,15 +1390,15 @@ main :: () { if is_equal_to_any(args[it], "--export-csv", "-e") { it += 1; if it >= args.count { - print_error("Missing CSV file path to export."); + log_error("Missing CSV file path to export."); exit(1); } if (load_database(*database, db_file_path) == false) { - print_error("Failed to load database."); + log_error("Failed to load database during export."); exit(1); } if (export_to_csv(*database, args[it]) == false) { - print_error("Failed to export CSV file."); + log_error("Failed to export CSV file."); exit(1); } reset_database(*database); @@ -1409,7 +1409,7 @@ main :: () { if is_equal_to_any(args[it], "--start-of-week", "-s") { it += 1; if it >= args.count { - print_error("Missing number for starting day of week."); + log_error("Missing number for starting day of week."); exit(1); } first_day_of_week = parse_int(*args[it]); @@ -1421,7 +1421,7 @@ main :: () { continue; } - print_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]); + log_error("%: invalid option '%'.\nTry '% --help' for more information.", args[0], args[it], args[0]); exit(1); } @@ -1431,11 +1431,11 @@ main :: () { } if (load_database(*database, db_file_path) == false) { - print_error("Failed to load database."); + log_error("Failed to load database."); exit(1); } - initialize_tui(); + initialize_user_interface(); db := *database; layout := *layouts[Layouts.COMPACT]; @@ -1511,9 +1511,14 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont if (countdown_to_autosave <= 0) { show_processing(); if (db == *archive) { - export_to_csv(*archive, ar_file_path); + 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."); } - store_database(database, db_file_path); + hide_processing(); } } @@ -1653,8 +1658,8 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont // 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); + if is_assign set_task_time(db, db.selected_idx, day, time); + else add_task_time(db, db.selected_idx, day, time); trigger_autosave(); @@ -1691,10 +1696,7 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont continue; } - if (add_task(db, selected_task) == null) { - print_error("Failed to duplicate task."); - continue; - } + add_task(db, selected_task); trigger_autosave(); // Refresh totals. @@ -1721,14 +1723,14 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont if (db == *database) { if (import_from_csv(*archive, ar_file_path) == false) { reset_database(*archive); - print_error("Failed to load archive."); + log_error("Failed to load archive."); continue; } db = *archive; } else { if (export_to_csv(*archive, ar_file_path) == false) { - print_error("Failed to store archive."); + log_error("Failed to store archive."); continue; } reset_database(*archive); @@ -1741,7 +1743,7 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont if (db != *database || selected_task == null || selected_task == active_task) continue; if (append_to_csv(selected_task, ar_file_path) == false) { - print_error("Failed to archive task."); + log_error("Failed to archive task."); continue; } delete_task(db, db.selected_idx); @@ -1758,10 +1760,7 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont continue; } - if (add_task(*database, selected_task) == null) { - print_error("Failed to restore task."); - continue; - } + add_task(*database, selected_task); delete_task(db, db.selected_idx); trigger_autosave(); @@ -1773,7 +1772,7 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont show_processing(); sort_procedure: (a: Task, b: Task) -> s64; - active_task: Task = ifx db.active_idx >= 0 then db.tasks[db.active_idx] else .{}; + 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"; @@ -1809,7 +1808,7 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont quick_sort(db.tasks, sort_procedure); if db.active_idx >= 0 { - db.active_idx = find_similar_task(db, active_task); + db.active_idx = find_similar_task(db, prev_active_task); } trigger_autosave(); @@ -1820,23 +1819,29 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont 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) == false) { - print_error("Failed to archive task."); // TODO Improve this. + if append_to_csv(it, ar_file_path) { + reset_task_times(db, it_index); + } + else { + failed_to_archive = true; } - reset_task_times(db, it_index); } trigger_autosave(); + if failed_to_archive then log_error("Failed to archive tasks."); // Coalesce similar tasks. case #char "c"; #through; case #char "C"; - // TODO Active task is lost... 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 { @@ -1852,6 +1857,10 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont } 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; @@ -1876,31 +1885,28 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont // Save any unsaved changes. show_processing(); - error_saving := false; if (db == *archive) { - if (export_to_csv(archive, ar_file_path) == false) { - print_error("Failed to save archive."); - error_saving |= true; + 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) { - if (store_database(database, db_file_path) == false) { - print_error("Failed to save database."); - error_saving |= true; + 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; } } - if (error_saving) { - print_error("Press any key to close."); - draw_error_window(); - TUI.get_key(); - } assert(TUI.reset_terminal(), "Failed to reset TUI."); -#if DEBUG_MEMORY { return; -} else { - exit(xx ifx error_saving then 1 else 0); -} } - -- cgit v1.2.3 From 0ca96dad124b9a8935902f9c2f884bc63ee7e430 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 03:47:36 +0100 Subject: Using dynamic memory allocation for draw buffer/string builder. --- ttt.jai | 57 ++++++++++++++++++++++----------------------------------- 1 file changed, 22 insertions(+), 35 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 7fff02d..700ebbd 100644 --- a/ttt.jai +++ b/ttt.jai @@ -79,6 +79,7 @@ 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, @@ -593,10 +594,7 @@ load_database :: (db: *Database, path: string) -> success: bool #must { log_error("Unexpected content found at the end of file '%'.", path); return false; } - - // Make sure we have a valid selected index. - if db.tasks.count > 0 && db.selected_idx < 0 then db.selected_idx = 0; - + return true; } @@ -927,21 +925,14 @@ get_layout_index_from_day_index :: inline (day_index: int) -> int { dbg_average := 0; // DEBUG dbg_count := 0; // DEBUG -buffer: String_Builder; // TODO + draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) { auto_release_temp(); - /* TODO - It's not safe to use temporary memory here because the console resolution may increase and use more than what we have in temporary memory. - And temporary memory is configured at compile time. - We should dynamically allocate memory with some headroom and, at beggining of function... adjust it if necessary. - - // init_string_builder(*buffer, 100000); - // builder := buffer; - */ - builder := String_Builder.{ allocator = temporary_allocator }; - TUI.using_builder_as_output(*builder); + 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); @@ -994,7 +985,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) TUI.set_style(style_default); print_time(size_y, x_total_offset, total_time, layout.columns[L_TOTAL_IDX].width); - write_builder(*builder); + write_builder(*draw_string_builder); return; } @@ -1010,23 +1001,22 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) TUI.draw_box(1, 1, size_x, size_y); // Draw table grids. - // TODO Maybe this could be simplified? y = 1; x = 1; - TUI.tui_write_string(TUI.Commands.DrawingMode); // append(*builder, TUI.Commands.DrawingMode); TODO + 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); - TUI.tui_write_string(TUI.Drawings.TeeT); // TODO append(*builder, TUI.Drawings.TeeT); + write_string(TUI.Drawings.TeeT); for row: 2..size_y { TUI.set_cursor_position(x, row); - TUI.tui_write_string(TUI.Drawings.LineV); // TODO append(*builder, TUI.Drawings.LineV); + write_string(TUI.Drawings.LineV); } TUI.set_cursor_position(x, size_y); - TUI.tui_write_string(TUI.Drawings.TeeB); // TODO append(*builder, TUI.Drawings.TeeB); + write_string(TUI.Drawings.TeeB); } - TUI.tui_write_string(TUI.Commands.TextMode); // TODO append(*builder, TUI.Commands.TextMode); + write_string(TUI.Commands.TextMode); /////////////////////////////////////////////////////////////////////////// @@ -1038,7 +1028,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) x += 1; col = *layout.columns[L_TITLE_IDX]; TUI.set_cursor_position(x + col.alignment_offset, y); - TUI.tui_write_string(ifx db == *archive then layout.archive_title else col.header); // TODO append(*builder, ifx db == *archive then layout.archive_title else col.header); + write_string(ifx db == *archive then layout.archive_title else col.header); x += col.width; // Headers : days @@ -1058,7 +1048,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) } col = *layout.columns[L_DAYS_IDX + day_idx]; TUI.set_cursor_position(x + col.alignment_offset, y); - TUI.tui_write_string(col.header); // TODO append(*builder, col.header); + write_string(col.header); x += col.width; } TUI.set_style(style_default); @@ -1067,7 +1057,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) x += 1; col = *layout.columns[L_TOTAL_IDX]; TUI.set_cursor_position(x + col.alignment_offset, y); - TUI.tui_write_string(col.header); // TODO append(*builder, col.header); + write_string(col.header); /////////////////////////////////////////////////////////////////////////// @@ -1108,11 +1098,11 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) task_name := cast(string)task.name; task_name = truncate(task_name, column_width); TUI.set_cursor_position(x, y); - TUI.tui_write_string(task_name); // TODO append(*builder, task_name); + 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 }; - TUI.tui_write_string(paint_remaining); // TODO append(*builder, paint_remaining); + write_string(paint_remaining); x += column_width; @@ -1140,10 +1130,10 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) 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) { - TUI.tui_print(" %/% ", db.selected_idx + 1, db.tasks.count); + print(" %/% ", db.selected_idx + 1, db.tasks.count); } else { - TUI.tui_print("%", db.selected_idx + 1); + print("%", db.selected_idx + 1); } @@ -1177,7 +1167,7 @@ draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) x += 1; print_time(y, x, total_time, layout.columns[L_TOTAL_IDX].width); - write_builder(*builder); + write_builder(*draw_string_builder); } free_memory :: () { @@ -1466,11 +1456,8 @@ dbg_average = (dbg_sample + dbg_count * dbg_average) / (dbg_count + 1); // DEBUG dbg_count += 1; // DEBUG TUI.set_cursor_position(3, 1); TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, context.temporary_storage.total_bytes_occupied, context.temporary_storage.high_water_mark, context.temporary_storage.size); // DEBUG - // write_string(builder_to_string(*builder,, allocator = temporary_allocator)); draw_error_window(); - TUI.set_cursor_position(40, 1); - TUI.tui_print(">%<", redraw_all); } key := TUI.get_key(INPUT_TIMEOUT_MS); @@ -1493,12 +1480,12 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont selected_task := get_selected_task(db); active_task := get_active_task(db); selected_task_row: int; - { // TODO Recheck this code. + { 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 1 + else ifx (selected_idx < 0) then 2 else (selected_idx % layout_tasks_rows) + NUM_HEADER_ROWS + 1; } -- cgit v1.2.3 From c5bbb26a9fff9dd3623b9e12128999935bc4c6ab Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 03:48:44 +0100 Subject: Cleanup some debug code. --- ttt.jai | 17 ----------------- 1 file changed, 17 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 700ebbd..f9afd52 100644 --- a/ttt.jai +++ b/ttt.jai @@ -923,9 +923,6 @@ get_layout_index_from_day_index :: inline (day_index: int) -> int { return (day_index - first_day_of_week + NUM_WEEK_DAYS) % NUM_WEEK_DAYS; } -dbg_average := 0; // DEBUG -dbg_count := 0; // DEBUG - draw_user_interface :: (db: *Database, layout: *Layout, redraw_all: bool = true) { auto_release_temp(); @@ -1446,17 +1443,7 @@ main :: () { write_strings(INVALID_WINDOW_MESSAGE); } else { -start := current_time_monotonic(); // DEBUG - draw_user_interface(db, layout, redraw_all); - -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 -TUI.set_cursor_position(3, 1); -TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, context.temporary_storage.total_bytes_occupied, context.temporary_storage.high_water_mark, context.temporary_storage.size); // DEBUG - draw_error_window(); } @@ -1468,10 +1455,6 @@ TUI.tui_print("Average % us (% / % : % bytes) ---------", dbg_average/1000, cont update_times(*database); - - if key == #char "k" { dbg_average = 0; dbg_count = 0; redraw_all = false; continue; } // DEBUG - - /* TODO Remove `selected_task` and `active_task` and helper functions. Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. -- cgit v1.2.3 From e8c8132361c114bf6d90e3fb1f46004c3e09fecc Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 04:11:53 +0100 Subject: Reviewed import_from_csv procedure. --- ttt.jai | 75 +++++++++++------------------------------------------------------ 1 file changed, 12 insertions(+), 63 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index f9afd52..36271f2 100644 --- a/ttt.jai +++ b/ttt.jai @@ -627,90 +627,45 @@ export_to_csv :: (db: Database, path: string) -> success: bool #must { // Imports CSV file into database. // Returns success. import_from_csv :: (db: *Database, path: string) -> bool #must { - // TODO Review code. assert(db != null, ASSERT_NOT_NULL, "db"); assert(xx path, ASSERT_NOT_EMPTY, "path"); - error_code: s64; - - // Check file size TODO Read based on file size - //file_info: stat_t; - //error_code = sys_stat(path, *file_info); // TODO Check for error. - //size := file_info.st_size; - size := 0; - - success: bool; - map: Map_File_Info; - data: string; - is_using_map := false; - if size >= 1<<30 { - assert(false, "Parsing big files not implemented yet."); - } - else { - data, success = read_entire_file(path); - } - defer if is_using_map then map_entire_file_end(*map); else free(data.data); - csv := data; - if success == false { - log_error("Failed to read file '%' while loading database: ERROR_FROM_LOG", path); // TODO Get error from logger ?! - return false; - } - - - // TODO Helper function. advance :: inline (array: *[] $T, amount: int = 1) { assert(amount >= 0); assert(array.count >= amount); array.count -= amount; array.data += amount; } - - - // TODO Helper function. - consume_next_line :: (sp: *string) -> string, bool { - // To find the end of the line, we look for a linefeed character. - // We will trim a carriage return off the end if there is one there also. - // Thus this works on both 'dos' and 'unix'-style files. + // 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 { - // This is the last line; there may not have been a linefeed after that, - // but we still want to handle that data, so we return true if there was - // a nonzero amount of stuff there. - << sp = ""; - return s, (s.count > 0); } - // Chop the characters we are going to return from 'sp', - // which holds the remaining file data. advance(sp, result.count + 1); if result { - if result[result.count-1] == 13 result.count -= 1; // If there's a carriage return at the end, remove it by decrementing the string's length. + // 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; } - //Skip header line. - consume_next_line(*csv); - - next_line :: inline (csv: *string) -> line: string, success: bool { - for 0..csv.count { - if csv.data[it] == #char "\n" { - line: string = < bool #must { auto_release_temp(); line, success := consume_next_line(*csv); - // line, success := next_line(*csv); if success == false break; task: Task; @@ -733,11 +687,6 @@ import_from_csv :: (db: *Database, path: string) -> bool #must { task.times[it_index] = string_to_int(it); add_task(db, *task); - // TODO Check this old code and remove it if not necessary. - // if context.temporary_storage.total_bytes_occupied > (100<<20) { - // print("temp: %\n", context.temporary_storage.total_bytes_occupied >> 20); - // reset_temporary_storage(); - // } } } -- cgit v1.2.3 From 8b934ebb39e67f1a3c9940474f69d05102b836c6 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 04:31:29 +0100 Subject: Solved memory leak. --- ttt.jai | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 36271f2..3682bfd 100644 --- a/ttt.jai +++ b/ttt.jai @@ -37,7 +37,7 @@ VERSION :: "2.0"; // Use only 3 chars (to fit layouts). YEAR :: "2024"; NUM_WEEK_DAYS :: 7; -APP_FOLDER_NAME :: ".task_time_tracker_test"; // TODO Using different folder to avoid erasing my work data. +APP_FOLDER_NAME :: ".task_time_tracker"; DB_FILE_NAME :: "database.bin"; AR_FILE_NAME :: "archive.csv"; DB_FILE_SIGN_STR :: "TTT:B:02"; @@ -436,9 +436,10 @@ update_times :: (db: *Database) { // Keep track of this update. db.modified_on = stop_time; - if db.active_idx < 0 return; + active_task := get_active_task(db); + + if active_task == null return; - active_task := *db.tasks[db.active_idx]; start_week_day: s8; while (start_time < stop_time) { @@ -657,12 +658,15 @@ import_from_csv :: (db: *Database, path: string) -> bool #must { return result, true; } - csv, success := read_entire_file(path); + data, success := read_entire_file(path); if success == false { log_error("Failed to read file '%'.", path); return false; } - defer free(csv.data); + defer free(data); + + // Work on a string struct copy, otherwise the free(data) will fail. + csv := data; // Skip header line. consume_next_line(*csv); @@ -1403,12 +1407,7 @@ main :: () { redraw_all = key != TUI.Keys.None; update_times(*database); - - /* TODO - Remove `selected_task` and `active_task` and helper functions. - Every time we add or remove tasks to the database, it may be reallocated, thus making the selected_task and active_task pointers invalid. Check if this is messing up the app. - Maybe use a macro returns the selected/active task based on the indices. - */ + selected_task := get_selected_task(db); active_task := get_active_task(db); selected_task_row: int; -- cgit v1.2.3 From b8cb706e8d9566425ca336e135e6f3ca93b6d467 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 04:36:40 +0100 Subject: Fixed type on error message. --- ttt.jai | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 3682bfd..6f165a8 100644 --- a/ttt.jai +++ b/ttt.jai @@ -568,7 +568,7 @@ load_database :: (db: *Database, path: string) -> success: bool #must { 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.\n"); + log_error("Invalid file signature while loading database."); return false; } -- cgit v1.2.3 From 1e5b1478a5baccb4972ce8dc321b114814829fcd Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 04:41:04 +0100 Subject: Improved help text. --- ttt.jai | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index 6f165a8..afa769e 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1245,13 +1245,13 @@ main :: () { "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", + " -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 a duplicate and reset times for all tasks.\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", -- cgit v1.2.3 From f513010b1a6d252c41183a16c4f71d519e70e3c3 Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 11 May 2024 04:51:50 +0100 Subject: Code cleanup on import_from_csv. --- ttt.jai | 40 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 21 deletions(-) (limited to 'ttt.jai') diff --git a/ttt.jai b/ttt.jai index afa769e..65a3cda 100644 --- a/ttt.jai +++ b/ttt.jai @@ -671,27 +671,25 @@ import_from_csv :: (db: *Database, path: string) -> bool #must { // Skip header line. consume_next_line(*csv); - { // Parse CSV lines. - line := csv; - while (true) { - auto_release_temp(); - - line, success := consume_next_line(*csv); - if success == false 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); - } + // 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. -- cgit v1.2.3 From 6c383e1c5c4ac4772df4edba027c71c9ddd9948b Mon Sep 17 00:00:00 2001 From: dam Date: Sun, 12 May 2024 02:42:47 +0100 Subject: Added licensing model. --- LICENSE | 1 + LICENSE.ISC | 15 +++++++++++++++ LICENSE.MIT | 21 +++++++++++++++++++++ ttt.jai | 19 ------------------- 4 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 LICENSE create mode 100644 LICENSE.ISC create mode 100644 LICENSE.MIT (limited to 'ttt.jai') diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..88afb49 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +SPDX-License-Identifier: MIT OR ISC diff --git a/LICENSE.ISC b/LICENSE.ISC new file mode 100644 index 0000000..3ca0ef1 --- /dev/null +++ b/LICENSE.ISC @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2024 Daniel Almeida Martins + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/LICENSE.MIT b/LICENSE.MIT new file mode 100644 index 0000000..1632077 --- /dev/null +++ b/LICENSE.MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Daniel Almeida Martins + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ttt.jai b/ttt.jai index 65a3cda..56410e5 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1,22 +1,3 @@ -// Copyright 2023 Daniel Martins -// License GPL-3.0-or-later -// -// This program is free software: you can redistribute it and/or modify it under -// the terms of the GNU General Public License as published by the Free Software -// Foundation, either version 3 of the License, or (at your option) any later -// version. -// -// This program is distributed in the hope that it will be useful, but WITHOUT -// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along with -// this program. If not, see . - -// Compilation commands: -// - release : jai ttt.jai -quiet -x64 -release -// - debug : jai ttt.jai -quiet -x64 - DEBUG_MEMORY :: true; #if DEBUG_MEMORY { #import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. -- cgit v1.2.3 From 10881d3088623f36bf4046a3d80bf9f513f54df6 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 13 May 2024 15:56:18 +0100 Subject: Finish last TODO. --- Test_IntSatArith.jai | 2 +- ttt.jai | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) (limited to 'ttt.jai') diff --git a/Test_IntSatArith.jai b/Test_IntSatArith.jai index 8e9125c..2128f7b 100644 --- a/Test_IntSatArith.jai +++ b/Test_IntSatArith.jai @@ -4,7 +4,7 @@ #import "Compiler"; #import "Math"; #import "String"; -#load "Integer_Saturating_Arithmetic.jai"; +#import "Integer_Saturating_Arithmetic"; main :: () { diff --git a/ttt.jai b/ttt.jai index 56410e5..bb395fa 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1,9 +1,6 @@ -DEBUG_MEMORY :: true; -#if DEBUG_MEMORY { -#import "Basic"()(MEMORY_DEBUGGER=true); // TODO Remove after final debug sessions. This takes up ~30MB of RAM. -} else { -#import "Basic"; -} +DEBUG :: false; + +#import "Basic"()(MEMORY_DEBUGGER=DEBUG); // Enabling memory debug adds ~30MB of RAM. #import "System"; #import "Sort"; #import "Math"; @@ -25,7 +22,6 @@ DB_FILE_SIGN_STR :: "TTT:B:02"; ASSERT_NOT_NULL :: "Parameter '%' is null."; ASSERT_NOT_EMPTY :: "Parameter '%' is empty."; -ASSERT_NOT_CONTAIN :: "'%' does not contain '%'."; ASSERT_INVALID_INDEX:: "Invalid index '%'."; SECONDS_IN_MINUTE :: cast(s64)60; @@ -1168,9 +1164,7 @@ prompt_user_key :: (y: int, message: string) -> TUI.Key { main :: () { -#if DEBUG_MEMORY { - defer report_memory_leaks(); // TODO Remove after final debug sessions. -} + #if DEBUG { defer report_memory_leaks(); } context.logger = print_error; -- cgit v1.2.3 From bdea73c8349c2c918befad4120f49f9826d270dc Mon Sep 17 00:00:00 2001 From: dam Date: Thu, 16 May 2024 02:42:04 +0100 Subject: Imported new module structure. --- Test_IntSatArith.jai | 304 ---------------------- modules/Integer_Saturating_Arithmetic.jai | 416 ------------------------------ modules/Saturation/module.jai | 416 ++++++++++++++++++++++++++++++ modules/Saturation/tests.jai | 304 ++++++++++++++++++++++ modules/TUI/key_map.jai | 401 ++++++++++++++-------------- modules/TUI/tests.jai | 19 +- modules/UTF8.jai | 128 --------- modules/UTF8/module.jai | 128 +++++++++ modules/UTF8/tests.jai | 5 + sizeof.c | 50 ---- ttt.jai | 2 +- 11 files changed, 1057 insertions(+), 1116 deletions(-) delete mode 100644 Test_IntSatArith.jai delete mode 100644 modules/Integer_Saturating_Arithmetic.jai create mode 100644 modules/Saturation/module.jai create mode 100644 modules/Saturation/tests.jai delete mode 100644 modules/UTF8.jai create mode 100644 modules/UTF8/module.jai create mode 100644 modules/UTF8/tests.jai delete mode 100644 sizeof.c (limited to 'ttt.jai') diff --git a/Test_IntSatArith.jai b/Test_IntSatArith.jai deleted file mode 100644 index 2128f7b..0000000 --- a/Test_IntSatArith.jai +++ /dev/null @@ -1,304 +0,0 @@ -// Tests for integer saturating arighmetic procedures. - -#import "Basic"; -#import "Compiler"; -#import "Math"; -#import "String"; -#import "Integer_Saturating_Arithmetic"; - -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 { - - print_test_call :: (operation: string) -> string { - str: string = ---; - if operation != "div" { - TEST_CALL :: #string DONE - t_result, t_saturated := OP(cast(Tx)x, cast(Ty)y); - if result != t_result print("%_%(%, %) = %0%0\n", operation, type, x, y, result, ifx saturated then " : saturated"); - DONE - str = replace(TEST_CALL, "OP", operation); - } else { - TEST_CALL :: #string DONE - t_result, t_remainder, t_saturated := OP(cast(Tx)x, cast(Ty)y); - if result != t_result print("%_%(%, %) = % + %0%0\n", operation, type, x, y, result, remainder, ifx saturated then " : saturated"); - DONE - str = replace(TEST_CALL, "OP", operation); - } - return str; - } - - #insert #run print_test_call(operation); - - errors := 0; - if result != t_result { errors += 1; print(" > incorrect result value: got % expected %\n", t_result, result); }; - if type != type_of(t_result) { errors += 1; print(" > incorrect result type: got % expected %\n", type_of(t_result), type); }; - if saturated != t_saturated { errors += 1; print(" > incorrect saturated flag: got % expected %\n", t_saturated, saturated); }; - #if operation == "div" { - if remainder != t_remainder { errors += 1; print(" > incorrect remainder value: got % expected %\n", t_remainder, remainder); }; - } - 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"); - - 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], true);", "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]);", "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/Integer_Saturating_Arithmetic.jai b/modules/Integer_Saturating_Arithmetic.jai deleted file mode 100644 index 74643e0..0000000 --- a/modules/Integer_Saturating_Arithmetic.jai +++ /dev/null @@ -1,416 +0,0 @@ -// 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/Saturation/module.jai b/modules/Saturation/module.jai new file mode 100644 index 0000000..74643e0 --- /dev/null +++ b/modules/Saturation/module.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/Saturation/tests.jai b/modules/Saturation/tests.jai new file mode 100644 index 0000000..372d66d --- /dev/null +++ b/modules/Saturation/tests.jai @@ -0,0 +1,304 @@ +// Tests for integer saturating arighmetic procedures. + +#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 { + + print_test_call :: (operation: string) -> string { + str: string = ---; + if operation != "div" { + TEST_CALL :: #string DONE + t_result, t_saturated := OP(cast(Tx)x, cast(Ty)y); + if result != t_result print("%_%(%, %) = %0%0\n", operation, type, x, y, result, ifx saturated then " : saturated"); + DONE + str = replace(TEST_CALL, "OP", operation); + } else { + TEST_CALL :: #string DONE + t_result, t_remainder, t_saturated := OP(cast(Tx)x, cast(Ty)y); + if result != t_result print("%_%(%, %) = % + %0%0\n", operation, type, x, y, result, remainder, ifx saturated then " : saturated"); + DONE + str = replace(TEST_CALL, "OP", operation); + } + return str; + } + + #insert #run print_test_call(operation); + + errors := 0; + if result != t_result { errors += 1; print(" > incorrect result value: got % expected %\n", t_result, result); }; + if type != type_of(t_result) { errors += 1; print(" > incorrect result type: got % expected %\n", type_of(t_result), type); }; + if saturated != t_saturated { errors += 1; print(" > incorrect saturated flag: got % expected %\n", t_saturated, saturated); }; + #if operation == "div" { + if remainder != t_remainder { errors += 1; print(" > incorrect remainder value: got % expected %\n", t_remainder, remainder); }; + } + 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"); + + 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], true);", "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]);", "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/key_map.jai b/modules/TUI/key_map.jai index 7a074b2..f0754a7 100644 --- a/modules/TUI/key_map.jai +++ b/modules/TUI/key_map.jai @@ -8,10 +8,11 @@ setup_key_map :: () { /* This table was created/tested using the following terminals: - - g: gnome (terminal) + - g: gnome terminal - i: kitty - k: konsole - - l: linux (console) + - l: linux console + - w: windows terminal - x: xterm To signal modifier keys, a letter is appended after a + (plus sign): @@ -33,16 +34,16 @@ setup_key_map :: () { "#f1+Z" -> F1 + Shift + Alt + Ctrl + Super */ - // Up // g i k l x - table_set(*key_map, "\e[A", to_key("#up")); // + + + + + + // 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;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")); // + @@ -52,16 +53,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[B", to_key("#down")); // + + + + + + // 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;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")); // + @@ -71,16 +72,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[C", to_key("#right")); // + + + + + + // 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;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")); // + @@ -90,16 +91,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[D", to_key("#left")); // + + + + + + // 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;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")); // + @@ -109,17 +110,17 @@ setup_key_map :: () { 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 x + // 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[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;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")); // + @@ -129,17 +130,17 @@ setup_key_map :: () { 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 x + // 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[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;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")); // + @@ -149,16 +150,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[2~", to_key("#ins")); // + + + + + + // 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;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")); // + @@ -168,16 +169,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[3~", to_key("#del")); // + + + + + + // 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;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")); // + @@ -187,16 +188,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[5~", to_key("#pup")); // + + + + + + // 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;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")); // + @@ -206,16 +207,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[6~", to_key("#pdown")); // + + + + + + // 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;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")); // + @@ -225,10 +226,10 @@ setup_key_map :: () { 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 x + // 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, "\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")); // + @@ -239,13 +240,13 @@ setup_key_map :: () { 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;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")); // + @@ -255,10 +256,10 @@ setup_key_map :: () { 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 x + // 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, "\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")); // + @@ -269,13 +270,13 @@ setup_key_map :: () { 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;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")); // + @@ -285,10 +286,10 @@ setup_key_map :: () { 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 x + // 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, "\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")); // + @@ -299,13 +300,13 @@ setup_key_map :: () { 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;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")); // + @@ -315,10 +316,10 @@ setup_key_map :: () { 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 x + // 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, "\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")); // + @@ -329,13 +330,13 @@ setup_key_map :: () { 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;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")); // + @@ -345,18 +346,18 @@ setup_key_map :: () { 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 x + // 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~", 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;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")); // + @@ -366,17 +367,17 @@ setup_key_map :: () { 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 x + // 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~", 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;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")); // + @@ -386,17 +387,17 @@ setup_key_map :: () { 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 x + // 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~", 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;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")); // + @@ -406,17 +407,17 @@ setup_key_map :: () { 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 x + // 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~", 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;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")); // + @@ -426,16 +427,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[20~", to_key("#f9")); // + + + + + + // 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;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")); // + @@ -445,16 +446,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[21~", to_key("#f10")); // + + + + + + // 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;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")); // + @@ -464,16 +465,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[23~", to_key("#f11")); // + + + + + + // 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;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")); // + @@ -483,16 +484,16 @@ setup_key_map :: () { 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 x - table_set(*key_map, "\e[24~", to_key("#f12")); // + + + + + + // 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;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")); // + diff --git a/modules/TUI/tests.jai b/modules/TUI/tests.jai index fe213cb..a740e6b 100644 --- a/modules/TUI/tests.jai +++ b/modules/TUI/tests.jai @@ -31,21 +31,6 @@ main :: () { assert_result(x == X && y == Y, "Failed set/get cursor position.\n"); } - if 1 { - print("TEST : module logger\n", to_standard_error = true); - log("- log: before module start."); - assert(TUI.setup_terminal(), "Failed to setup TUI."); - TUI.set_cursor_position(3, 3); - print("wait"); - sleep_milliseconds(500); - log("- log: while module is active."); - sleep_milliseconds(500); - print(" a bit"); - sleep_milliseconds(1000); - assert(TUI.reset_terminal(), "Failed to reset TUI."); - log("- log: after module stop."); - } - if 1 { print("TEST : test key input\n", to_standard_error = true); auto_release_temp(); @@ -121,7 +106,7 @@ main :: () { print("TEST : print keys\n", to_standard_error = true); auto_release_temp(); assert(TUI.setup_terminal(), "Failed to setup TUI."); - key: TUI.Key = #char "d"; + key: TUI.Key = TUI.Keys.None; last_none_char := "X"; width, height := TUI.get_terminal_size(); @@ -135,7 +120,7 @@ main :: () { 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)"); + 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; diff --git a/modules/UTF8.jai b/modules/UTF8.jai deleted file mode 100644 index 72d3d75..0000000 --- a/modules/UTF8.jai +++ /dev/null @@ -1,128 +0,0 @@ -// 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 (leading_byte: u8) -> int { - // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte - if (leading_byte & 0xE0) == 0xC0 return 1+1; - - // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte - if (leading_byte & 0xF0) == 0xE0 return 1+2; - - // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte - if (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; -} diff --git a/modules/UTF8/module.jai b/modules/UTF8/module.jai new file mode 100644 index 0000000..72d3d75 --- /dev/null +++ b/modules/UTF8/module.jai @@ -0,0 +1,128 @@ +// 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 (leading_byte: u8) -> int { + // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte + if (leading_byte & 0xE0) == 0xC0 return 1+1; + + // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte + if (leading_byte & 0xF0) == 0xE0 return 1+2; + + // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte + if (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; +} diff --git a/modules/UTF8/tests.jai b/modules/UTF8/tests.jai new file mode 100644 index 0000000..aff1d36 --- /dev/null +++ b/modules/UTF8/tests.jai @@ -0,0 +1,5 @@ +#import "Basic"; + +main :: () { + assert(false, "TODO"); // TODO +} diff --git a/sizeof.c b/sizeof.c deleted file mode 100644 index c260c31..0000000 --- a/sizeof.c +++ /dev/null @@ -1,50 +0,0 @@ -// compile with : gcc sizeof.c -lncurses - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char **argv) { - // initscr(); - - fprintf(stderr, "sizeof char: %d\n", sizeof(char)); - fprintf(stderr, "sizeof short: %d\n", sizeof(short)); - fprintf(stderr, "sizeof int: %d\n", sizeof(int)); - fprintf(stderr, "sizeof long: %d\n", sizeof(long)); - fprintf(stderr, "sizeof unsigned: %d\n", sizeof(unsigned)); - fprintf(stderr, "sizeof chtype: %d\n", sizeof(chtype)); - // int w_size_x, w_size_y; - // getmaxyx(stdscr, w_size_y, w_size_x); - // char str[64]; - // memset(str, 0, 64); - // sprintf(str, "x,y : %dx%d\n", w_size_x, w_size_y); - // mvaddstr(2, 2, str); - // sprintf(str, "resize:%d\n", KEY_RESIZE); - // mvaddstr(3, 2, str); - - // unsigned m = ACS_DIAMOND; - fprintf(stderr, "sizeof ACS %d\n", sizeof(ACS_DIAMOND)); - // - // if (ACS_DIAMOND != 0 || ACS_URCORNER != 0){ - // fprintf(stderr, "BAZINGA\n"); - // } -// fprintf(stderr, ">%d<\n", strlen(ACS_DIAMOND)); - - // mvaddch(0, 0, m); - // getch(); - // endwin(); -} - diff --git a/ttt.jai b/ttt.jai index bb395fa..b0d60d0 100644 --- a/ttt.jai +++ b/ttt.jai @@ -7,7 +7,7 @@ DEBUG :: false; #import "File"; #import "File_Utilities"; #import "String"; -#import "Integer_Saturating_Arithmetic"; +#import "Saturation"; #import "UTF8"; TUI :: #import "TUI"(COLOR_MODE_BITS=4); -- cgit v1.2.3 From af1317ed93f7d5fc16baceaadb0da17aa17591be Mon Sep 17 00:00:00 2001 From: dam Date: Sat, 18 May 2024 01:30:01 +0100 Subject: Updated modules. --- modules/Saturation/module.jai | 26 +-- modules/Saturation/tests.jai | 397 +++++++++++++++++++++++++++++++++++++++--- modules/TUI/module.jai | 17 +- modules/TUI/snake.jai | 165 ++++++++++++++++++ modules/UTF8/module.jai | 29 ++- modules/UTF8/tests.jai | 159 ++++++++++++++++- snake.jai | 165 ------------------ ttt.jai | 2 +- 8 files changed, 748 insertions(+), 212 deletions(-) create mode 100644 modules/TUI/snake.jai delete mode 100644 snake.jai (limited to 'ttt.jai') diff --git a/modules/Saturation/module.jai b/modules/Saturation/module.jai index 74643e0..50c9b3c 100644 --- a/modules/Saturation/module.jai +++ b/modules/Saturation/module.jai @@ -1,5 +1,7 @@ // 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"; @@ -37,11 +39,11 @@ INTEGER_ARITHMETIC_TYPES_CHECK :: #string DONE return true; DONE -add :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +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 USE_GENERIC || CPU != .X64 { - #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { #if Tr == s8 { MAX :: S8_MAX; MIN :: S8_MIN; } @@ -66,7 +68,7 @@ add :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b return x + y, false; } else { - + result: Tr = ---; saturated: bool = ---; @@ -118,10 +120,10 @@ add :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b } } -sub :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +sub :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } { - #if USE_GENERIC || CPU != .X64 { + #if !(prefer_branch_free_code && CPU == .X64) { #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { @@ -195,10 +197,10 @@ sub :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b } -mul :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +mul :: (x: $Tx, y: $Ty, $prefer_branch_free_code := PREFER_BRANCH_FREE_CODE) -> result: $Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } { - #if USE_GENERIC || CPU != .X64 { + #if !(prefer_branch_free_code && CPU == .X64) { #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { @@ -286,10 +288,10 @@ mul :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, saturated: b } } -div :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, remainder: Tr, saturated: bool #modify { #insert INTEGER_ARITHMETIC_TYPES_CHECK; } +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 USE_GENERIC || CPU != .X64 { + #if !(prefer_branch_free_code && CPU == .X64) { #if Tr == s8 || Tr == s16 || Tr == s32 || Tr == s64 { @@ -380,11 +382,11 @@ div :: (x: $Tx, y: $Ty, $USE_GENERIC: bool = false) -> result: $Tr, remainder: T #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. + mov.SIZE result, x; // Copy x value to result. DIVIDE_PLACEHOLDER } diff --git a/modules/Saturation/tests.jai b/modules/Saturation/tests.jai index 372d66d..2a82300 100644 --- a/modules/Saturation/tests.jai +++ b/modules/Saturation/tests.jai @@ -1,5 +1,7 @@ // Tests for integer saturating arighmetic procedures. +AVOID_INFINITE_FOR_LOOP :: true; + #import "Basic"; #import "Compiler"; #import "Math"; @@ -12,36 +14,41 @@ main :: () { "#=======================#\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); - test_op :: (operation: string, x: $Tx, y: $Ty, result: $Tr, type: Type, saturated: bool, remainder: Tr = 0) -> errors_found: int #expand { - - print_test_call :: (operation: string) -> string { - str: string = ---; - if operation != "div" { - TEST_CALL :: #string DONE - t_result, t_saturated := OP(cast(Tx)x, cast(Ty)y); - if result != t_result print("%_%(%, %) = %0%0\n", operation, type, x, y, result, ifx saturated then " : saturated"); - DONE - str = replace(TEST_CALL, "OP", operation); - } else { - TEST_CALL :: #string DONE - t_result, t_remainder, t_saturated := OP(cast(Tx)x, cast(Ty)y); - if result != t_result print("%_%(%, %) = % + %0%0\n", operation, type, x, y, result, remainder, ifx saturated then " : saturated"); - DONE - str = replace(TEST_CALL, "OP", operation); - } - return str; - } - - #insert #run print_test_call(operation); + return builder_to_string(*builder); + }(); errors := 0; - if result != t_result { errors += 1; print(" > incorrect result value: got % expected %\n", t_result, result); }; - if type != type_of(t_result) { errors += 1; print(" > incorrect result type: got % expected %\n", type_of(t_result), type); }; - if saturated != t_saturated { errors += 1; print(" > incorrect saturated flag: got % expected %\n", t_saturated, saturated); }; + 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(" > incorrect remainder value: got % expected %\n", t_remainder, remainder); }; + 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; } @@ -157,6 +164,342 @@ main :: () { 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" @@ -212,11 +555,11 @@ main :: () { 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], true);", "OP", operation); + 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]);", "OP", operation); + 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); diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index fb742da..d0373f9 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -1,3 +1,16 @@ +/* + 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); @@ -32,11 +45,11 @@ #import "UTF8"; #load "key_map.jai"; -#add_context tui_style : Style; // This contains the last style applied by the module. +#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. +#assert(input_buffer.count >= KEY_SIZE); // The input buffer size must be capable to hold an entire Key. active := false; input_override : Key; diff --git a/modules/TUI/snake.jai b/modules/TUI/snake.jai new file mode 100644 index 0000000..b35c0fb --- /dev/null +++ b/modules/TUI/snake.jai @@ -0,0 +1,165 @@ +#import "Basic"; +#import "Random"; +TUI :: #import "TUI"(COLOR_MODE_BITS = 8); + +Vec2D :: struct { + x: int; + y: int; +} + +operator == :: (a: Vec2D, b: Vec2D) -> bool { + return a.x == b.x && a.y == b.y; +} + +screen_size_x: int = ---; +screen_size_y: int = ---; +player_name: string = ---; + +main :: () { + + game_loop :: () { + + LOOP_PERIOD_MS :: 30; + + 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; + + TUI.flush_input(); + TUI.set_next_key(TUI.Keys.Resize); + timer := current_time_monotonic(); + while main_loop := true { + + timestamp := current_time_monotonic(); + key := TUI.get_key(LOOP_PERIOD_MS); + + if key == { + case TUI.Keys.Resize; + 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); + write_strings(" ", player_name, " "); + + case #char "q"; #through; + case #char "Q"; #through; + case TUI.Keys.Escape; + break main_loop; + + case TUI.Keys.Up; + if dir != Vec2D.{0, 1} then dir = Vec2D.{0, -1}; + + case TUI.Keys.Down; + if dir != Vec2D.{0, -1} then dir = Vec2D.{0, 1}; + + case TUI.Keys.Left; + if dir != Vec2D.{1, 0} then dir = Vec2D.{-1, 0}; + + case TUI.Keys.Right; + if dir != Vec2D.{-1, 0} then dir = Vec2D.{1, 0}; + } + + 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; + } + + last_pos := snake_parts[snake_parts.count-1]; + + // Update 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, }); + TUI.set_cursor_position(food.x, food.y); + write_string(TUI.Drawings.Diamond); + } + write_string(TUI.Commands.TextMode); + + // Set score + TUI.set_cursor_position(3, 1); + print(" % ", score); + } + } + + GAME_OVER_TEXT :: "~ game over ~"; + INSTRUCTIONS_TEXT :: "(esc to exit)"; + + seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. + random_seed(seed); + + assert(TUI.setup_terminal(), "Failed to setup TUI."); + TUI.set_cursor_position(1, 1); + + write_string("Please enter player name: "); + player_name = TUI.read_input_line(64); + + while true { + game_loop(); + + // Game over screen. + box_size := Vec2D.{19, 4}; + TUI.draw_box((screen_size_x-box_size.x)/2, (screen_size_y-box_size.y)/2, box_size.x, box_size.y); + 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-GAME_OVER_TEXT.count)/2, (screen_size_y-box_size.y)/2 + 2); + write_string(INSTRUCTIONS_TEXT); + sleep_milliseconds(100); + TUI.flush_input(); + if TUI.get_key() == TUI.Keys.Escape then break; + } + + assert(TUI.reset_terminal(), "Failed to reset TUI."); +} diff --git a/modules/UTF8/module.jai b/modules/UTF8/module.jai index 72d3d75..5e6fd65 100644 --- a/modules/UTF8/module.jai +++ b/modules/UTF8/module.jai @@ -8,15 +8,15 @@ is_continuation_byte :: inline (byte: u8) -> bool { } // Given a leading_byte, returns the number of bytes on the character. -count_character_bytes :: inline (leading_byte: u8) -> int { +count_character_bytes :: inline (character_leading_byte: u8) -> int { // BBBB BBBB & 1110 0000 == 110X XXXX -> 1 initial + 1 continuation byte - if (leading_byte & 0xE0) == 0xC0 return 1+1; + if (character_leading_byte & 0xE0) == 0xC0 return 1+1; // BBBB BBBB & 1111 0000 == 1110 XXXX -> 1 initial + 2 continuation byte - if (leading_byte & 0xF0) == 0xE0 return 1+2; + if (character_leading_byte & 0xF0) == 0xE0 return 1+2; // BBBB BBBB & 1111 1000 == 1111 0XXX -> 1 initial + 3 continuation byte - if (leading_byte & 0xF8) == 0xF0 return 1+3; + if (character_leading_byte & 0xF8) == 0xF0 return 1+3; return 1; } @@ -126,3 +126,24 @@ get_byte_index :: (str: string, character_index: int) -> buffer_index: int, succ } 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 index aff1d36..b7e3579 100644 --- a/modules/UTF8/tests.jai +++ b/modules/UTF8/tests.jai @@ -1,5 +1,162 @@ #import "Basic"; +#import "String"; +#import "UTF8"; main :: () { - assert(false, "TODO"); // TODO + 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/snake.jai b/snake.jai deleted file mode 100644 index b35c0fb..0000000 --- a/snake.jai +++ /dev/null @@ -1,165 +0,0 @@ -#import "Basic"; -#import "Random"; -TUI :: #import "TUI"(COLOR_MODE_BITS = 8); - -Vec2D :: struct { - x: int; - y: int; -} - -operator == :: (a: Vec2D, b: Vec2D) -> bool { - return a.x == b.x && a.y == b.y; -} - -screen_size_x: int = ---; -screen_size_y: int = ---; -player_name: string = ---; - -main :: () { - - game_loop :: () { - - LOOP_PERIOD_MS :: 30; - - 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; - - TUI.flush_input(); - TUI.set_next_key(TUI.Keys.Resize); - timer := current_time_monotonic(); - while main_loop := true { - - timestamp := current_time_monotonic(); - key := TUI.get_key(LOOP_PERIOD_MS); - - if key == { - case TUI.Keys.Resize; - 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); - write_strings(" ", player_name, " "); - - case #char "q"; #through; - case #char "Q"; #through; - case TUI.Keys.Escape; - break main_loop; - - case TUI.Keys.Up; - if dir != Vec2D.{0, 1} then dir = Vec2D.{0, -1}; - - case TUI.Keys.Down; - if dir != Vec2D.{0, -1} then dir = Vec2D.{0, 1}; - - case TUI.Keys.Left; - if dir != Vec2D.{1, 0} then dir = Vec2D.{-1, 0}; - - case TUI.Keys.Right; - if dir != Vec2D.{-1, 0} then dir = Vec2D.{1, 0}; - } - - 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; - } - - last_pos := snake_parts[snake_parts.count-1]; - - // Update 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, }); - TUI.set_cursor_position(food.x, food.y); - write_string(TUI.Drawings.Diamond); - } - write_string(TUI.Commands.TextMode); - - // Set score - TUI.set_cursor_position(3, 1); - print(" % ", score); - } - } - - GAME_OVER_TEXT :: "~ game over ~"; - INSTRUCTIONS_TEXT :: "(esc to exit)"; - - seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. - random_seed(seed); - - assert(TUI.setup_terminal(), "Failed to setup TUI."); - TUI.set_cursor_position(1, 1); - - write_string("Please enter player name: "); - player_name = TUI.read_input_line(64); - - while true { - game_loop(); - - // Game over screen. - box_size := Vec2D.{19, 4}; - TUI.draw_box((screen_size_x-box_size.x)/2, (screen_size_y-box_size.y)/2, box_size.x, box_size.y); - 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-GAME_OVER_TEXT.count)/2, (screen_size_y-box_size.y)/2 + 2); - write_string(INSTRUCTIONS_TEXT); - sleep_milliseconds(100); - TUI.flush_input(); - if TUI.get_key() == TUI.Keys.Escape then break; - } - - assert(TUI.reset_terminal(), "Failed to reset TUI."); -} diff --git a/ttt.jai b/ttt.jai index b0d60d0..d5d6e4e 100644 --- a/ttt.jai +++ b/ttt.jai @@ -7,7 +7,7 @@ DEBUG :: false; #import "File"; #import "File_Utilities"; #import "String"; -#import "Saturation"; +#import "Saturation"(PREFER_BRANCH_FREE_CODE=true); #import "UTF8"; TUI :: #import "TUI"(COLOR_MODE_BITS=4); -- cgit v1.2.3 From 22e3b95ce23808ee08a5fa3b0481c17c2e5506d6 Mon Sep 17 00:00:00 2001 From: dam Date: Mon, 20 May 2024 23:19:55 +0100 Subject: Updated TUI module to clear inside error message box. --- modules/TUI/module.jai | 26 ++++++-- modules/TUI/snake.jai | 165 ------------------------------------------------- ttt.jai | 2 +- 3 files changed, 21 insertions(+), 172 deletions(-) delete mode 100644 modules/TUI/snake.jai (limited to 'ttt.jai') diff --git a/modules/TUI/module.jai b/modules/TUI/module.jai index d0373f9..2fb1233 100644 --- a/modules/TUI/module.jai +++ b/modules/TUI/module.jai @@ -125,10 +125,21 @@ Commands :: struct #type_info_none { // Draw/text. DrawingMode :: "\e(0"; TextMode :: "\e(B"; - ClearScreen :: "\e[2J"; - ClearLine :: "\e[2K"; + 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%@"; @@ -536,12 +547,12 @@ read_input_line :: (count_limit: int, is_visible: bool = true) -> string, Key { if is_visible { print_to_builder(*builder, Commands.SetCursorPosition, y, x); append(*builder, str); - for chars_count..count_limit-1 append(*builder, " "); + 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, "*"); - for chars_count..count_limit-1 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); @@ -615,7 +626,7 @@ flush_input :: () { input_string.count = 0; } -draw_box :: (x: int, y: int, width: int, height: int) { +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."); @@ -641,10 +652,13 @@ draw_box :: (x: int, y: int, width: int, height: int) { 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); diff --git a/modules/TUI/snake.jai b/modules/TUI/snake.jai deleted file mode 100644 index b35c0fb..0000000 --- a/modules/TUI/snake.jai +++ /dev/null @@ -1,165 +0,0 @@ -#import "Basic"; -#import "Random"; -TUI :: #import "TUI"(COLOR_MODE_BITS = 8); - -Vec2D :: struct { - x: int; - y: int; -} - -operator == :: (a: Vec2D, b: Vec2D) -> bool { - return a.x == b.x && a.y == b.y; -} - -screen_size_x: int = ---; -screen_size_y: int = ---; -player_name: string = ---; - -main :: () { - - game_loop :: () { - - LOOP_PERIOD_MS :: 30; - - 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; - - TUI.flush_input(); - TUI.set_next_key(TUI.Keys.Resize); - timer := current_time_monotonic(); - while main_loop := true { - - timestamp := current_time_monotonic(); - key := TUI.get_key(LOOP_PERIOD_MS); - - if key == { - case TUI.Keys.Resize; - 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); - write_strings(" ", player_name, " "); - - case #char "q"; #through; - case #char "Q"; #through; - case TUI.Keys.Escape; - break main_loop; - - case TUI.Keys.Up; - if dir != Vec2D.{0, 1} then dir = Vec2D.{0, -1}; - - case TUI.Keys.Down; - if dir != Vec2D.{0, -1} then dir = Vec2D.{0, 1}; - - case TUI.Keys.Left; - if dir != Vec2D.{1, 0} then dir = Vec2D.{-1, 0}; - - case TUI.Keys.Right; - if dir != Vec2D.{-1, 0} then dir = Vec2D.{1, 0}; - } - - 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; - } - - last_pos := snake_parts[snake_parts.count-1]; - - // Update 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, }); - TUI.set_cursor_position(food.x, food.y); - write_string(TUI.Drawings.Diamond); - } - write_string(TUI.Commands.TextMode); - - // Set score - TUI.set_cursor_position(3, 1); - print(" % ", score); - } - } - - GAME_OVER_TEXT :: "~ game over ~"; - INSTRUCTIONS_TEXT :: "(esc to exit)"; - - seed: u64 = xx to_milliseconds(current_time_monotonic()) | 0x01; // Seed must be odd. - random_seed(seed); - - assert(TUI.setup_terminal(), "Failed to setup TUI."); - TUI.set_cursor_position(1, 1); - - write_string("Please enter player name: "); - player_name = TUI.read_input_line(64); - - while true { - game_loop(); - - // Game over screen. - box_size := Vec2D.{19, 4}; - TUI.draw_box((screen_size_x-box_size.x)/2, (screen_size_y-box_size.y)/2, box_size.x, box_size.y); - 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-GAME_OVER_TEXT.count)/2, (screen_size_y-box_size.y)/2 + 2); - write_string(INSTRUCTIONS_TEXT); - sleep_milliseconds(100); - TUI.flush_input(); - if TUI.get_key() == TUI.Keys.Escape then break; - } - - assert(TUI.reset_terminal(), "Failed to reset TUI."); -} diff --git a/ttt.jai b/ttt.jai index d5d6e4e..1bfabd0 100644 --- a/ttt.jai +++ b/ttt.jai @@ -141,7 +141,7 @@ draw_error_window :: () { 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); + 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); -- cgit v1.2.3 From ec706533ca26d49670adb97617df0d565528e395 Mon Sep 17 00:00:00 2001 From: dam Date: Wed, 29 May 2024 12:49:49 +0100 Subject: Updated documentation and licenses for release. --- LICENSE-GPL-3.0-or-later | 674 +++++++++++++++++++++++++++++++++++++++++ LICENSE-ISC | 15 - LICENSE-MIT | 21 -- README.md | 202 +++++++----- modules/LICENSE-ISC | 15 + modules/LICENSE-MIT | 21 ++ modules/README.md | 34 +++ modules/TUI/examples/snake.jai | 188 ++++++++++++ ttt.jai | 3 + 9 files changed, 1058 insertions(+), 115 deletions(-) create mode 100644 LICENSE-GPL-3.0-or-later delete mode 100644 LICENSE-ISC delete mode 100644 LICENSE-MIT create mode 100644 modules/LICENSE-ISC create mode 100644 modules/LICENSE-MIT create mode 100644 modules/README.md create mode 100644 modules/TUI/examples/snake.jai (limited to 'ttt.jai') diff --git a/LICENSE-GPL-3.0-or-later b/LICENSE-GPL-3.0-or-later new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE-GPL-3.0-or-later @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/LICENSE-ISC b/LICENSE-ISC deleted file mode 100644 index 3ca0ef1..0000000 --- a/LICENSE-ISC +++ /dev/null @@ -1,15 +0,0 @@ -ISC License - -Copyright (c) 2024 Daniel Almeida Martins - -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. diff --git a/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index 1632077..0000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Daniel Almeida Martins - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index 57a2544..6167659 100644 --- a/README.md +++ b/README.md @@ -1,84 +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 MIT or ISC. - -SPDX-License-Identifier: MIT OR ISC - -# 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 to-do items; -- [x] Hide stderr messages from app screen. -- [x] Improve error detection/messages. +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/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/ttt.jai b/ttt.jai index 1bfabd0..67d6e18 100644 --- a/ttt.jai +++ b/ttt.jai @@ -1,3 +1,6 @@ +// 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. -- cgit v1.2.3