// Compilation command: // - release: gcc main.c -Wall -O2 -m64 -lncurses -o ttt // - debug : gcc main.c -Wall -g3 -m64 -lncurses -o ttt -D DEBUG #include #include #include #include #include #include #include #include #include #include #define STR_(X) #X // Convert to string. #define STR(X) STR_(X) // Force argument expansions before converting to string. #define MAX_TASK_NAME 58 // Maximum task name length, including null-terminator. #define FIRST_DAY_OF_WEEK 1 // (0-6, Sunday = 0) #define LOG_FILE_NAME "log.txt" #define DB_BIN_PATH_NAME "./database.bin" #define DB_CSV_PATH_NAME "./database.csv" #define DB_MAX_CAP ((PTRDIFF_MAX >> 1) + 1) typedef struct /*__attribute__((__packed__))*/ { uint32_t times[7]; char name[MAX_TASK_NAME]; } task_t; typedef struct /*__attribute__((__packed__))*/ { time_t modified_on; size_t count; size_t capacity; task_t *tasks; ptrdiff_t active_task; ptrdiff_t selected_task; // TODO Maybe use this instead of indexes? } database_t; const char *DB_FILE_SIGN = "TTT:B:01"; const size_t SIZEOF_DB_FILE_SIGN = sizeof(DB_FILE_SIGN); const size_t SIZEOF_TASK_T = sizeof(task_t); const size_t SIZEOF_DATABASE_T = sizeof(database_t); database_t database; database_t archive; // TODO To be implemented in the future. // Given an UTF8 encoded string, truncate it to length without breaking any UTF8 character. // The string should have capacity for at least length number of items. // The terminating null byte ('\0') is included in length. // The function returns the amount of items that got discarded counting from length. size_t truncate_string_utf8(char *string, size_t length) { // Check for special cases where no truncation is required. if (length == 0 || string[length-1] == '\0') { return 0; } // Search for a non-UTF8-sequence-item so we can truncate the string. size_t idx = length - 1; while(idx > 0 && ((string[idx] & 0xC0) == 0x80)) { idx--; } string[idx] = '\0'; return length - idx; } // 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; } void print_task(const task_t *task) { printf("name: '%s'\n", task->name); printf("t[0]: '%" PRIu32 "'\n", task->times[0]); printf("t[1]: '%" PRIu32 "'\n", task->times[1]); printf("t[2]: '%" PRIu32 "'\n", task->times[2]); printf("t[3]: '%" PRIu32 "'\n", task->times[3]); printf("t[4]: '%" PRIu32 "'\n", task->times[4]); printf("t[5]: '%" PRIu32 "'\n", task->times[5]); printf("t[6]: '%" PRIu32 "'\n", task->times[6]); } char* format_time(uint32_t time, char* string) { const time_t seconds_per_day = 25*60*60; if (time > 24*60*60) { // ---.-- d // │ 3│ // │ │ sprintf(string, "%5.2fd", (double)time / (double)seconds_per_day); } else if (time > 60) { double hours = (double)time / 3600.0; double minutes = (time - (time_t)hours) / 60.0; sprintf(string, "%02.0f:%02.0f", hours, minutes); } else { sprintf(string, "%2dsec", time); } return string; } // Creates new task returned in the pointer. If necessary, expands database capacity. // Returns success. bool create_task(database_t *db, task_t **task) { assert(db != NULL); if (db->count == DB_MAX_CAP) { fprintf(stderr, "Database reached maximum capacity.\n"); 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 > DB_MAX_CAP >> 1 ? DB_MAX_CAP : // Protect against DB_MAX_CAP != power-of-two. current_capacity << 1; task_t *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_T); if (new_tasks == NULL) { fprintf(stderr, "Failed to expand database.\n"); return false; } db->capacity = new_capacity; db->tasks = new_tasks; } // Prepare new task. *task = &db->tasks[db->count]; memset(*task, 0, SIZEOF_TASK_T); db->count++; // Adjust selected task. if (db->selected_task < 0) { db->selected_task = db->count-1; } return true; } bool delete_task(database_t *db, task_t *task) { assert(db != NULL); assert(task != NULL); assert(task >= db->tasks && task < &db->tasks[db->count]); // 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_T); 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_t *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_T); if (new_tasks == NULL) { fprintf(stderr, "Failed to shrink database.\n"); return false; } db->capacity = new_capacity; db->tasks = new_tasks; } return true; } void clear_database(database_t *db) { free(db->tasks); memset(db, 0, SIZEOF_DATABASE_T); } // Stores data from database into binary file. // Returns success. bool store_database(const database_t *db, const char *path_name) { assert(db != NULL); assert(path_name != NULL); // Open file. FILE *file = fopen(path_name, "w"); if (file == NULL) { fprintf(stderr, "Failed to open file '%s' while storing database: %s.\n", path_name, strerror(errno)); return false; } fwrite(DB_FILE_SIGN, sizeof(char), SIZEOF_DB_FILE_SIGN, file); fwrite(db, SIZEOF_DATABASE_T, 1, file); fwrite(db->tasks, SIZEOF_TASK_T, db->count, file); fclose(file); return true; } // Loads data from binary file into database. // Returns success. bool load_database(database_t *db, const char *path_name) { assert(db != NULL); assert(path_name != NULL); // Open file. FILE *file = fopen(path_name, "r"); if (file == NULL) { fprintf(stderr, "Failed to open file '%s' while loading database: %s.\n", path_name, strerror(errno)); return false; } // Validate file signature. char file_signature[SIZEOF_DB_FILE_SIGN]; fread(&file_signature, sizeof(char), SIZEOF_DB_FILE_SIGN, file); if (strncmp(file_signature, DB_FILE_SIGN, SIZEOF_DB_FILE_SIGN) != 0) { fprintf(stderr, "Invalid file signature.\n"); return false; } // Read database structure. fread(db, SIZEOF_DATABASE_T, 1, file); // Restore database capacity. db->tasks = calloc(db->capacity, SIZEOF_TASK_T); // Read database entries. fread(db->tasks, SIZEOF_TASK_T, 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_t *db, const char *path_name) { assert(db != NULL); assert(path_name != NULL); FILE *file = fopen(path_name, "w"); if (file == NULL) { fprintf(stderr, "Failed to open file '%s' while exporting to CSV: %s.\n", path_name, 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[MAX_TASK_NAME]; task_t *limit = db->tasks + db->count; for (task_t *task = db->tasks; task < limit; task++) { memcpy(name, task->name, MAX_TASK_NAME); replace_char(name, ',', ' '); fprintf(file, "%s,%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 "\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_t *db, const char *path_name) { assert(db != NULL); assert(path_name != NULL); FILE *file = fopen(path_name, "r"); if (file == NULL) { fprintf(stderr, "Failed to open file '%s' while importing from CSV: %s.\n", path_name, strerror(errno)); return false; } uint32_t number_of_entries = 0; task_t *task; char *line_buffer = NULL; size_t line_buffer_size = 0; ssize_t read_characters = 0; char *name_delimiter; // Skip header line. fscanf(file, "%*[^\n]\n"); // Parse CSV file. while(true) { read_characters = getline(&line_buffer, &line_buffer_size, file); // Check if reached EOF. if (read_characters == -1) { break; } // Prepare new task. create_task(db, &task); // Find task name string limits. name_delimiter = strchr(line_buffer, ','); size_t name_length = (name_delimiter - line_buffer) + 1; if (name_length > MAX_TASK_NAME) { name_length = MAX_TASK_NAME; } // Import task name. memcpy(task->name, line_buffer, name_length); truncate_string_utf8(task->name, name_length); // Parse task times. if(sscanf(name_delimiter+1, "%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32, &task->times[0], &task->times[1], &task->times[2], &task->times[3], &task->times[4], &task->times[5], &task->times[6] ) != 7) { replace_char(line_buffer, '\n', ' '); fprintf(stderr, "Discarding invalid line '%s' and continuing.\n", line_buffer); delete_task(db, task); continue; } // Add new database entry. number_of_entries++; } fclose(file); free(line_buffer); return true; } enum TEST { T_NONE = 0x00, T_TIME = 0x01, T_XXXX = 0x02, T_SBIN = 0x04, T_LBIN = 0x08, T_ECSV = 0x10, T_ICSV = 0x20, T_SOS = 0x40, T_ALL = 0xFF, }; void prototype(int level) { const char *done = "# -- done -- -- -- /\n"; /////////////////////////////////////////////////////////////////////////// // Get current time and day of week (UTC). if (level & T_TIME) { fprintf(stderr, "# UTC time and day of week -------------------- \\\n"); time_t now_utc = time(NULL); // Get current UTC time. uint8_t week_day = localtime(&now_utc)->tm_wday; // Get current day of the week. fprintf(stderr, "day of week: %d\ntime: %s", week_day, ctime(&now_utc)); fprintf(stderr, done); } /////////////////////////////////////////////////////////////////////////// // Prepare some data for testing. /* task_t tmp[] = { { .name = "ALPHA-TASK", .times = { 0, 0, 0, 0, 0, 0, 0 }, }, { .name = "BETA-TASK", .times = { 1, 1, 1, 1, 1, 1, 1 }, }, { .name = "DELTA-TASK", .times = { 2, 2, 2, 2, 2, 2, 2 }, } }; uint32_t number_of_items = sizeof(tmp) / sizeof(task_t); database.tasks = calloc(number_of_items, SIZEOF_TASK_T); database.capacity = number_of_items; database.count = number_of_items; memcpy(database.tasks, &tmp, SIZEOF_TASK_T * number_of_items); */ /////////////////////////////////////////////////////////////////////////// // Store database from memory to binary file. if (level & T_SBIN) { fprintf(stderr, "# store database ------------------------------ \\\n"); store_database(&database, DB_BIN_PATH_NAME); fprintf(stderr, done); } /////////////////////////////////////////////////////////////////////////// // Load database from binary file to memory. if (level & T_LBIN) { fprintf(stderr, "# load database ------------------------------- \\\n"); database_t db_load; memset(&db_load, 0, SIZEOF_DATABASE_T); load_database(&db_load, DB_BIN_PATH_NAME); fprintf(stderr, "loaded %zu entries.\n", db_load.count); for (uint32_t idx = 0; idx < db_load.count; idx++) { print_task(&db_load.tasks[idx]); } clear_database(&db_load); fprintf(stderr, done); } /////////////////////////////////////////////////////////////////////////// // Export database to CSV file. if (level & T_ECSV) { fprintf(stderr, "# export to CSV ------------------------------- \\\n"); export_to_csv(&database, DB_CSV_PATH_NAME); fprintf(stderr, done); } /////////////////////////////////////////////////////////////////////////// // Import database from CSV file. if (level & T_ICSV) { fprintf(stderr, "# import from CSV ----------------------------- \\\n"); database_t db_import; memset(&db_import, 0, SIZEOF_DATABASE_T); import_from_csv(&db_import, DB_CSV_PATH_NAME); fprintf(stderr, "imported %zu entries.\n", db_import.count); for (uint32_t idx = 0; idx < db_import.count; idx++) { print_task(&db_import.tasks[idx]); } clear_database(&db_import); fprintf(stderr, done); } /////////////////////////////////////////////////////////////////////////// // Check size of structs if (level & T_SOS) { size_t size; char *name; fprintf(stderr, "# check structs sizes ------------------------- \\\n"); name = "database_t"; size = sizeof(database_t); fprintf(stderr, "sizeof(%s) = %zu bytes (%zu bits : %6.3f W64b)\n", name, size, size*8, ((double)size)*8.0/64.0); name = "task_t"; size = sizeof(task_t); fprintf(stderr, "sizeof(%s) = %zu bytes (%zu bits : %6.3f W64b)\n", name, size, size*8, ((double)size)*8.0/64.0); name = "time_t"; size = sizeof(time_t); fprintf(stderr, "sizeof(%s) = %zu bytes (%zu bits : %6.3f W64b)\n", name, size, size*8, ((double)size)*8.0/64.0); fprintf(stderr, done); } } void update_timers(database_t *db) { // Get current UTC time. time_t stop_time = time(NULL); // Get last modified on UTC time. time_t start_time = db->modified_on; // time_t diff = (double)stop_time - start_time; // fprintf(stderr, "> diff: %zu seconds | %6.2f minutes | %6.2f hours\n", (time_t)diff, diff / 60.0, diff / (60.0*60.0)); if (db->active_task < 0) { return; } task_t *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 week of day. time_t next_day = (start_time / 86400) * 86400 + 86400; time_t next_start = next_day < stop_time ? next_day : stop_time; active_task->times[start_week_day] += next_start - start_time; start_time = next_start; } db->modified_on = stop_time; } char *line_buffer; int size_x, size_y, pos_x, pos_y; uint8_t selected_layout = 0; #define NUM_OF_COLUMNS 9 typedef struct { int columns_size; char *table_headers[NUM_OF_COLUMNS]; int column_widths[NUM_OF_COLUMNS]; char alignments[NUM_OF_COLUMNS]; } layout_t; layout_t *layouts = NULL; void initialize_tui() { layouts = calloc(2, sizeof(layout_t)); // Layout : 0 : normal. // TODO Headers must be dynamic according to FIRST_DAY_OF_WEEK layouts[0] = (layout_t) { .columns_size = 8, .column_widths = { -1, 7, 7, 7, 7, 7, 7, 7, 9 }, .alignments = { 'L', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C' }, .table_headers = { " Task Time Tracker v1 ", " Sun ", " Mon ", " Tue ", " Wed ", " Thu ", " Fri ", " Sat ", " Total ", }, }; // Layout : 1 : compact. // TODO Headers must be dynamic according to FIRST_DAY_OF_WEEK layouts[1] = (layout_t){ .columns_size = 6, .column_widths = { -1, 5, 5, 5, 5, 5, 5, 5, 5 }, .alignments = { 'L', 'C', 'C', 'C', 'C', 'C', 'C', 'C', 'C' }, .table_headers = { " TTT v1 ", " S ", " M ", " T ", " W ", " T ", " F ", " S ", " # ", }, }; 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. start_color(); init_pair(1, COLOR_BLUE, COLOR_BLACK); init_pair(2, COLOR_BLACK, COLOR_CYAN); init_pair(3, COLOR_WHITE, COLOR_BLUE); } void initialization() { if (load_database(&database, DB_BIN_PATH_NAME) == false) { memset(&database, 0, SIZEOF_DATABASE_T); } } void free_memory() { clear_database(&database); free(line_buffer); line_buffer = NULL; free(layouts); layouts = NULL; } void draw_tui() { layout_t *layout = &layouts[selected_layout]; int columns_size = layout->columns_size; char **table_headers = layout->table_headers; time_t now_utc = time(NULL); uint8_t today = localtime(&now_utc)->tm_wday; int color_pair = 0; // The first column expands to fill the remaining space (is dynamic). layout->column_widths[0] = size_x - (NUM_OF_COLUMNS - 1) - 2; for (int idx = 1; idx < NUM_OF_COLUMNS; idx++) { layout->column_widths[0] -= layout->column_widths[idx]; } clear(); // Draw outer border. box(stdscr, 0, 0); // Draw column grids. int x = 0; for (int idx = 0; idx < NUM_OF_COLUMNS - 1; idx++) { x += 1 + layout->column_widths[idx]; mvaddch(0, x, ACS_TTEE); for (int y = 1; y < size_y - 1; y++) { mvaddch(y, x, ACS_VLINE); } mvaddch(size_y - 1, x, ACS_BTEE); } // Draw headers. // TODO Missing some spacing on the initial column. x = 0; for (int idx = 0; idx < NUM_OF_COLUMNS; idx++) { x += 1; int header_position = x; switch(layout->alignments[idx]) { case 'L': break; case 'C': header_position += ((layout->column_widths[idx] - strlen(layout->table_headers[idx])) / 2); break; case 'R': header_position += (layout->column_widths[idx] - strlen(layout->table_headers[idx])); break; } mvaddstr(0, header_position, layout->table_headers[idx]); x += layout->column_widths[idx]; } // Draw tasks. int y = 0; for (task_t *task = database.tasks; task < database.tasks + database.count; task++) { y++; int x = 0; x++; mvaddstr(y, x, task->name); // x += 1 + layouts->column_widths[0]; // mvaddstr(y, x, } // WIP TODO TASK BUG TEST HACK WARNING return; int start_columns = size_x - 1 - 8*columns_size; int columns[] = { 0, start_columns+(columns_size*0), start_columns+(columns_size*1), start_columns+(columns_size*2), start_columns+(columns_size*3), start_columns+(columns_size*4), start_columns+(columns_size*5), start_columns+(columns_size*6), start_columns+(columns_size*7), }; mvaddstr(0, columns[0]+2, table_headers[0]); for (int idx = 1; idx < 9; idx++) { // TODO When does the week start? mvaddch(0, columns[idx], ACS_TTEE); if (idx == today) { attron(COLOR_PAIR(color_pair)); } // mvaddch(0, columns[idx]+1, ACS_RTEE); mvaddstr(0, columns[idx]+2, table_headers[idx]); // mvaddch(0, columns[idx+1]-1, ACS_LTEE); if (idx == today) { attroff(COLOR_PAIR(color_pair)); } } /////////////////////////////////////////////////////////////////////////// // Table task_t *x_task; task_t *active_task; task_t *selected_task; // Draw rows with tasks. move(1, 0); active_task = database.tasks + database.active_task; selected_task = database.tasks + database.selected_task; uint64_t total = 0; uint64_t totals[8] = {0, 0, 0, 0, 0, 0, 0, 0}; for (size_t idx = 0; idx < database.count; idx++){ x_task = &database.tasks[idx]; // Enable highlight color on selected entry. color_pair = 0; if (x_task == active_task) { color_pair = 1; } if (x_task == selected_task) { color_pair = 2; } if (x_task == active_task && x_task == selected_task) { color_pair = 3; } if (color_pair == 1 || color_pair == 3) { attron(A_BOLD); } attron(COLOR_PAIR(color_pair)); // Clear line_buffer and add string termination. memset(line_buffer, ' ', size_x * sizeof(char)); line_buffer[size_x-1] = '\0'; // Check maximum available space for task name and print it accordingly. size_t task_name_length = strlen(x_task->name)*sizeof(char); size_t max_column_length = start_columns - 2; size_t copy_length = task_name_length < max_column_length ? task_name_length : max_column_length; memcpy(line_buffer+2, x_task->name, copy_length); uint32_t *times = x_task->times; sprintf(line_buffer + columns[1], "%8d", times[0]); sprintf(line_buffer + columns[2], "%8d", times[1]); sprintf(line_buffer + columns[3], "%8d", times[2]); sprintf(line_buffer + columns[4], "%8d", times[3]); sprintf(line_buffer + columns[5], "%8d", times[4]); sprintf(line_buffer + columns[6], "%8d", times[5]); sprintf(line_buffer + columns[7], "%8d", times[6]); // char time_str[7]; // sprintf(line_buffer + columns[1], "%s", format_time(times[0], time_str)); // sprintf(line_buffer + columns[2], "%s", format_time(times[1], time_str)); // sprintf(line_buffer + columns[3], "%s", format_time(times[2], time_str)); // sprintf(line_buffer + columns[4], "%s", format_time(times[3], time_str)); // sprintf(line_buffer + columns[5], "%s", format_time(times[4], time_str)); // sprintf(line_buffer + columns[6], "%s", format_time(times[5], time_str)); // sprintf(line_buffer + columns[7], "%s", format_time(times[6], time_str)); // Update totals. total = 0; for(int idx = 7-1; idx >= 0; idx--) { total += times[idx]; totals[idx] += times[idx]; } totals[7] += total; sprintf(line_buffer + columns[8], "%6" PRIu64, total); // BUG This causes "corrupted size vs. prev_size because it is writing beyond line_buffer size. addstr(line_buffer); // Disable highlight color on selected entry. attroff(COLOR_PAIR(color_pair)); attroff(A_BOLD); // Print columns separators. pos_y = getcury(stdscr); for (int c_idx = 0; c_idx < sizeof(columns)/sizeof(int); c_idx++) { mvaddch(pos_y, columns[c_idx], ACS_VLINE); } // Go to next line. pos_y++; move(pos_y, 0); } // Print totals memset(line_buffer, ' ', size_x * sizeof(char)); line_buffer[size_x-1] = '\0'; sprintf(line_buffer + columns[1], "%8" PRIu64, totals[0]); sprintf(line_buffer + columns[2], "%8" PRIu64, totals[1]); sprintf(line_buffer + columns[3], "%8" PRIu64, totals[2]); sprintf(line_buffer + columns[4], "%8" PRIu64, totals[3]); sprintf(line_buffer + columns[5], "%8" PRIu64, totals[4]); sprintf(line_buffer + columns[6], "%8" PRIu64, totals[5]); sprintf(line_buffer + columns[7], "%8" PRIu64, totals[6]); mvaddstr(pos_y, 0, line_buffer); } int main(int argc, char *argv[]) { // Make sure architecture uses 8bits per char. assert(CHAR_BIT == 8); // TODO Parse commands using: https://stackoverflow.com/questions/9642732/parsing-command-line-arguments-in-c if (argc > 1) { char *action; bool do_action = false; for (int idx = 1; idx < argc; idx++) { action = "--version"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; action = "-v"; do_action |= strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { fprintf(stdout, "Task Time Tracker v1.0\n"); return EXIT_SUCCESS; } action = "--fake_bin"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { prototype(T_SBIN); return EXIT_SUCCESS; } action = "--t_time"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { prototype(T_TIME); return EXIT_SUCCESS; } action = "--t_sos"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { prototype(T_SOS); return EXIT_SUCCESS; } action = "--t_icsv"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { prototype(T_ICSV); return EXIT_SUCCESS; } action = "--t_ecsv"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { load_database(&database, DB_BIN_PATH_NAME); prototype(T_ECSV); return EXIT_SUCCESS; } action = "--t_lbin"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { prototype(T_LBIN); return EXIT_SUCCESS; } action = "--t_csv2bin"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { clear_database(&database); import_from_csv(&database, DB_CSV_PATH_NAME); store_database(&database, DB_BIN_PATH_NAME); return EXIT_SUCCESS; } action = "--test"; do_action = strncmp(argv[idx], action, strlen(action)) == 0; if (do_action) { prototype(T_ALL); return EXIT_SUCCESS; } } fprintf(stdout, "Unkown command '%s'.\nUse '%s --help' for list of commands.\n", argv[1], argv[0]); return EXIT_FAILURE; } initialization(); initialize_tui(); int ch; // TODO When this is active, it cancels selecting text with the mouse, and breaks the creation of a new task. timeout(10000); // Make getch() timeout after timeout(...) miliseconds. ch = KEY_RESIZE; do { task_t *active_task = database.tasks + database.active_task; task_t *selected_task = database.tasks + database.selected_task; update_timers(&database); switch(ch) { case ERR: // When getch() times out. break; case KEY_F(1): { task_t *new_task; if (create_task(&database, &new_task) == false) { // ERROR break; } int row = database.count; mvaddch(row, 0, ACS_DIAMOND); clrtoeol(); mvaddch(row, size_x-1, ACS_VLINE); curs_set(1); mvgetnstr(row, 2, new_task->name, MAX_TASK_NAME-1); // TODO Move this empty-name-cleaning code elsewhere. bool is_empty = true; int size = strlen(new_task->name); for (int idx=0; idx < size; idx++) { char name_char = new_task->name[idx]; if (name_char != ' ' && name_char != '\t') { is_empty = false; break; } } if (strlen(new_task->name) == 0 || is_empty) { strcpy(new_task->name, "-- new task --"); } new_task->name[MAX_TASK_NAME-1] = '\0'; char *name = new_task->name; truncate_string_utf8(name, MAX_TASK_NAME-1); curs_set(0); break; } case KEY_F(2): { if (database.selected_task < 0) { break; } // rename stuff int row = database.selected_task + 1; mvaddch(row, 0, ACS_DIAMOND); clrtoeol(); mvaddch(row, size_x-1, ACS_VLINE); curs_set(1); mvgetnstr(row, 2, selected_task->name, MAX_TASK_NAME-1); curs_set(0); break; } case KEY_F(3): { if (database.selected_task < 0) { break; } delete_task(&database, selected_task); break; } case '\n': case ' ': if (true) { task_t *next_task = selected_task; if (active_task > 0) { update_timers(&database); // TODO Should I keep this even though it always does? database.active_task = -1; } if (active_task != next_task) { database.active_task = next_task - database.tasks; } database.modified_on = time(NULL); store_database(&database, DB_BIN_PATH_NAME); } break; case KEY_RESIZE: erase(); getmaxyx(stdscr, size_y, size_x); line_buffer = realloc(line_buffer, size_x * sizeof(char)); break; case KEY_LEFT: break; case KEY_RIGHT: break; case KEY_UP: if (database.selected_task > 0) { database.selected_task--; } break; case KEY_DOWN: if (database.selected_task < database.count - 1) { database.selected_task++; } break; } if (size_x >= 60 && size_y > 2) { selected_layout = size_x > 100 ? 0 : 1; draw_tui(); } else { const char *INVALID_WINDOW_MESSAGE = "Please expand window."; mvaddstr(size_y / 2, (size_x - strlen(INVALID_WINDOW_MESSAGE)) / 2, INVALID_WINDOW_MESSAGE); } // TEST // if (ch == KEY_F(1) || ch == KEY_F(2)) { // mvprintw(1, 1, "db:'%u'/'%u'", database.count, database.capacity); // } } while((ch = getch()) != 'q'); update_timers(&database); store_database(&database, DB_BIN_PATH_NAME); free_memory(); endwin(); return EXIT_SUCCESS; }