// Compilation command: // - release: gcc main.c -Wall -pedantic -O2 -m64 -lncursesw -o ttt // - debug : gcc main.c -Wall -pedantic -g3 -m64 -lncursesw -o ttt -D DEBUG #include #include #include #include #include #include #include #include #include #include #include #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 AR_BIN_PATH_NAME "./archive.bin" typedef struct /*__attribute__((__packed__))*/ { int64_t times[7]; char name[MAX_TASK_NAME]; } task_t; typedef struct /*__attribute__((__packed__))*/ { task_t *tasks; size_t count; size_t capacity; ptrdiff_t active_task; ptrdiff_t selected_task; int64_t modified_on; int64_t total_times[7]; } database_t; #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_T = sizeof(task_t); const size_t SIZEOF_DATABASE_T = sizeof(database_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; database_t database; database_t archive; // 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; } char* format_time(intmax_t time, char* string, int length) { int left_padding = (length - 5) / 2; int right_padding = length - 5 - left_padding; if (time > (intmax_t)9999 * SECONDS_IN_YEAR) { sprintf(string, "%*s ∞ %*s", left_padding, "", right_padding, ""); } else if (time > (intmax_t)9999 * SECONDS_IN_DAY) { double value = (double)time / (double)SECONDS_IN_YEAR; int decimals = time > 99 * SECONDS_IN_YEAR ? 0 : time > 9 * SECONDS_IN_YEAR ? 1 : 2; sprintf(string, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, ""); } else if (time >= (intmax_t)100 * SECONDS_IN_HOUR) { double value = (double)time / (double)SECONDS_IN_DAY; int decimals = time > 99 * SECONDS_IN_DAY ? 0 : time > 9 * SECONDS_IN_DAY ? 1 : 2; sprintf(string, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, ""); } else if (time >= SECONDS_IN_MINUTE) { intmax_t hours = (double)time / (double)SECONDS_IN_HOUR; intmax_t minutes = (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE; sprintf(string, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, ""); } else if (time >= 0) { sprintf(string, "%*s%4jds%*s", left_padding, "", time, right_padding, ""); } else { sprintf(string, "%*s - %*s", left_padding, "", right_padding, ""); } return string; } int64_t add_time(int64_t x, int64_t y) { if (y > 0 && x > INT64_MAX - y) return INT64_MAX; if (y < 0 && x < INT64_MIN - y) return INT64_MIN; return x + y; } int64_t sub_time(int64_t x, int64_t y) { if (y < 0 && x > INT64_MAX + y) return INT64_MAX; if (y > 0 && x < INT64_MIN + y) return INT64_MIN; return x - y; } // Returns active task or NULL if none applies. task_t *get_active_task(database_t *db) { assert(db != NULL); task_t *task = NULL; if (db->active_task >= 0) { task = db->tasks + db->active_task; } return task; } // Returns selected task or NULL if none applies. task_t *get_selected_task(database_t *db) { assert(db != NULL); task_t *task = NULL; if (db->selected_task >= 0) { task = db->tasks + db->selected_task; } return task; } // 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 == PTRDIFF_MAX) { 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 > PTRDIFF_MAX >> 1 ? PTRDIFF_MAX : 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; } // Adds the given task to the database using (using create_task and memcpy). // Returns success. bool add_task(database_t *db, task_t *task) { assert(db != NULL); assert(task != NULL); task_t *new_task; if (create_task(db, &new_task) == false) { return false; } memcpy(new_task, task, SIZEOF_TASK_T); // Add task timer values to total timers. for (int idx = 0; idx < 7; idx++) { // db->total_times[idx] += task->times[idx]; TODO db->total_times[idx] = add_time(db->total_times[idx], task->times[idx]); } return true; } // Deletes the task provided in the pointer. If possible, shrinks the database capacity. // Returns success. bool delete_task(database_t *db, task_t *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 < 7; idx++) { // db->total_times[idx] -= task->times[idx]; TODO db->total_times[idx] = sub_time(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_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; } // Resets database to the initial state and deallocates all memory taken by tasks. void reset_database(database_t *db) { free(db->tasks); memset(db, 0, SIZEOF_DATABASE_T); db->active_task = -1; db->selected_task = -1; } // 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), DB_FILE_SIGN_LENGTH, 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[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) { 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,%" 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_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; } // Skip header line. fscanf(file, "%*[^\n]\n"); // Parse CSV file. char *line_buffer = NULL; size_t line_buffer_size = 0; while(getline(&line_buffer, &line_buffer_size, file) != -1) { // Check if reached EOF. // Find task name string limits. char *name_delimiter = strchr(line_buffer, ','); if (name_delimiter == NULL) { continue; } size_t name_length = (name_delimiter - line_buffer) + 1; if (name_length > MAX_TASK_NAME) { name_length = MAX_TASK_NAME; } // Prepare new task. task_t *task; create_task(db, &task); // 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, "%" 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] ) != 7) { replace_char(line_buffer, '\n', ' '); fprintf(stderr, "Discarding invalid line '%s' and continuing.\n", line_buffer); delete_task(db, task); continue; } // Add task timer values to total timers. for (int idx = 0; idx < 7; idx++) { // db->total_times[idx] += task->times[idx]; TODO db->total_times[idx] = add_time(db->total_times[idx], task->times[idx]); } } fclose(file); free(line_buffer); return true; } enum TEST { T_NONE = 0x00, T_TIME = 0x01, T_SOT = 0x02, T_TPF = 0x04, 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); } /////////////////////////////////////////////////////////////////////////// // Check size of types if (level & T_SOT) { size_t size; char *name; fprintf(stderr, "# check size of types ------------------------- \\\n"); fprintf(stderr, "sizeof(byte) = %u bits\n", CHAR_BIT); 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); name = "size_t"; size = sizeof(size_t); fprintf(stderr, "sizeof(%s) = %zu bytes (%zu bits : %6.3f W64b)\n", name, size, size*8, ((double)size)*8.0/64.0); name = "ptrdiff_t"; size = sizeof(ptrdiff_t); fprintf(stderr, "sizeof(%s) = %zu bytes (%zu bits : %6.3f W64b)\n", name, size, size*8, ((double)size)*8.0/64.0); name = "int"; size = sizeof(int); fprintf(stderr, "sizeof(%s) = %zu bytes (%zu bits : %6.3f W64b)\n", name, size, size*8, ((double)size)*8.0/64.0); name = "DB_FILE_SIGN_LENGTH"; size = DB_FILE_SIGN_LENGTH; 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); } /////////////////////////////////////////////////////////////////////////// // Check time print format if (level & T_TPF) { double times[7] = { 0.12345, 1.12345, 10.12345, 100.12345, 1000.12345, 10000.12345, 10000000000000.12345 }; for (int idx = 0; idx < 7; idx++) { fprintf(stderr, "%.2e\n", times[idx]); } } } 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; 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 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; } db->modified_on = stop_time; } void update_total_timers(database_t *db) { int64_t *d0 = &db->total_times[0]; int64_t *d1 = &db->total_times[1]; int64_t *d2 = &db->total_times[2]; int64_t *d3 = &db->total_times[3]; int64_t *d4 = &db->total_times[4]; int64_t *d5 = &db->total_times[5]; int64_t *d6 = &db->total_times[6]; memset(db->total_times, 7, sizeof(int64_t)); for (size_t idx = 0; idx < db->count; idx++) { int64_t *times = db->tasks[idx].times; *d0 = add_time(*d0, times[0]); *d1 = add_time(*d1, times[1]); *d2 = add_time(*d2, times[2]); *d3 = add_time(*d3, times[3]); *d4 = add_time(*d4, times[4]); *d5 = add_time(*d5, times[5]); *d6 = add_time(*d6, times[6]); } } 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 timers_offset; char *table_headers[NUM_OF_COLUMNS]; int column_widths[NUM_OF_COLUMNS]; char alignments[NUM_OF_COLUMNS]; int alignment_offsets[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) { .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){ .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 ", " # ", }, }; // Calculate alignment_offsets. for(layout_t *layout = layouts; layout <= layouts + 1; layout++) { for (int idx = 0; idx < NUM_OF_COLUMNS; idx++) { int offset; switch(layout->alignments[idx]) { default: case 'L': offset = 0; break; case 'C': offset = ((layout->column_widths[idx] - strlen(layout->table_headers[idx])) / 2); break; case 'R': offset = (layout->column_widths[idx] - strlen(layout->table_headers[idx])); break; } layout->alignment_offsets[idx] = 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. start_color(); init_pair(1, COLOR_BLUE, COLOR_BLACK); init_pair(2, COLOR_BLACK, COLOR_CYAN); init_pair(3, COLOR_WHITE, COLOR_BLUE); } void free_memory() { reset_database(&database); free(line_buffer); line_buffer = NULL; free(layouts); layouts = NULL; } void draw_tui(database_t *db) { layout_t *layout = &layouts[selected_layout]; // The first column expands to fill the remaining space dynamically. 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]; } // TODO Unsure if this is needed. erase(); // Draw outer border. box(stdscr, 0, 0); // Draw edit symbol on title when in archive mode. if (db == &archive) { mvaddch(0, 0, ACS_DIAMOND); } // 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; int idx_fdow; // TODO Index - first day of week. for (int idx = 0; idx < NUM_OF_COLUMNS; idx++) { x += 1; idx_fdow = idx; if (idx_fdow > 0 && idx_fdow < 8) { idx_fdow = ((idx - 1) + FIRST_DAY_OF_WEEK) % 7 + 1; } int header_position = x + layout->alignment_offsets[idx_fdow]; mvaddstr(0, header_position, layout->table_headers[idx_fdow]); x += layout->column_widths[idx_fdow]; } // Draw tasks. uint64_t total_time = 0; int times_offset = 1; int column_width; int y = 0; int available_rows = size_y - 2; task_t *active_task = get_active_task(db); task_t *selected_task = get_selected_task(db); // This is some sort of pagination to allow scrolling through the tasks. task_t *start_task = db->tasks + (db->selected_task / available_rows) * available_rows; for (task_t *task = start_task; available_rows > 0 && task < db->tasks + db->count; task++) { available_rows--; y++; // TEST -- START // Enable highlight color on selected entry. int color_pair = 0; { if (task == active_task) { color_pair = 1; } if (task == selected_task) { color_pair = 2; } if (task == active_task && task == selected_task) { color_pair = 3; } if (color_pair == 1 || color_pair == 3) { attron(A_BOLD); } attron(COLOR_PAIR(color_pair)); } // TEST -- STOP int x = 0; // Task name. x++; column_width = layout->column_widths[0]; sprintf(line_buffer, "%*s", column_width, ""); // TODO Which one should I use? // memset(line_buffer, ' ', column_width); // line_buffer[column_width] = '\0'; mvaddnstr(y, x, line_buffer, column_width); mvaddnstr(y, x, task->name, column_width); // TODO Trim at utf8-char using not-yet-implemented-function. x += layout->column_widths[0]; // Task times. total_time = 0; for (int idx = 0; idx < 7; idx++) { x++; idx_fdow = (idx + FIRST_DAY_OF_WEEK) % 7; int column_width = layout->column_widths[idx_fdow + times_offset]; int64_t task_time = task->times[idx_fdow]; // total_time += task_time; TODO total_time = add_time(total_time, task_time); if (task_time > 0) { format_time(task_time, line_buffer, column_width); } else { sprintf(line_buffer, "%*s", column_width, ""); } mvaddstr(y, x, line_buffer); x += column_width; } // Task total. x++; column_width = layout->column_widths[8]; // TODO format_time(total_time, line_buffer, column_width); mvaddstr(y, x, line_buffer); // TEST -- START attroff(COLOR_PAIR(color_pair)); attroff(A_BOLD); // TEST -- STOP // Show current selected task and total number of tasks. sprintf(line_buffer, " %td/%zd ", db->selected_task+1, db->count); if (strlen(line_buffer) > layout->column_widths[0]) { sprintf(line_buffer, "%td", db->selected_task+1); } mvaddstr(size_y-1, 1, line_buffer); } // Daily totals. y = size_y-1; x = 0 + 1 + layout->column_widths[0]; total_time = 0; for (int idx = 0; idx < 7; idx++) { x++; idx_fdow = (idx + FIRST_DAY_OF_WEEK) % 7; int64_t daily_total = db->total_times[idx_fdow]; column_width = layout->column_widths[idx_fdow + times_offset]; // total_time += daily_total; TODO total_time = add_time(total_time, daily_total); format_time(daily_total, line_buffer, column_width); mvaddstr(y, x, line_buffer); x += column_width; } x++; column_width = layout->column_widths[7 + times_offset]; format_time(total_time, line_buffer, column_width); mvaddstr(y, x, line_buffer); x += column_width; } database_t *db; int main(int argc, char *argv[]) { // clock_t start, end; // double cpu_time_used; // // start = clock(); // load_database(&database, DB_BIN_PATH_NAME); // end = clock(); // cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC; // fprintf(stderr, "load: %f\n", cpu_time_used); // // start = clock(); // update_total_timers(&database); // end = clock(); // cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC; // fprintf(stderr, "calc: %f\n", cpu_time_used); // return 0; reset_database(&database); reset_database(&archive); db = &database; 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)+1) == 0; if (do_action) { fprintf(stdout, "Task Time Tracker v1.0\n"); return EXIT_SUCCESS; } action = "--t_time"; do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { prototype(T_TIME); return EXIT_SUCCESS; } action = "--t_sot"; do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { prototype(T_SOT); return EXIT_SUCCESS; } action = "--t_tpf"; do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { prototype(T_TPF); return EXIT_SUCCESS; } action = "--test"; do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { prototype(T_ALL); return EXIT_SUCCESS; } action = "--icsv"; do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { if (argc < idx+1) { fprintf(stdout, "Missing CSV file path to import.\n"); } load_database(&database, DB_BIN_PATH_NAME); import_from_csv(&database, argv[idx+1]); store_database(&database, DB_BIN_PATH_NAME); return EXIT_SUCCESS; } action = "--ecsv"; do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { if (argc < idx+1) { fprintf(stdout, "Missing CSV file path to export.\n"); } load_database(&database, DB_BIN_PATH_NAME); export_to_csv(&database, argv[idx+1]); return EXIT_SUCCESS; } } fprintf(stdout, "Unkown command '%s'.\nUse '%s --help' for list of commands.\n", argv[1], argv[0]); return EXIT_FAILURE; } initialize_tui(); if (load_database(&database, DB_BIN_PATH_NAME) == false) { store_database(&database, DB_BIN_PATH_NAME); } // TODO // When this is active, it cancels selecting text with the mouse, and breaks the creation of a new task. // Fortunatelly, this only happens when we write on the line being selected. If we only update the places that changes, this problem goes away. timeout(1000); // Make getch() timeout after timeout(...) miliseconds. int ch = KEY_RESIZE; do { task_t *active_task = get_active_task(db); task_t *selected_task = get_selected_task(db); update_timers(&database); switch(ch) { // When getch() times out. case ERR: break; // When terminal is resized. case KEY_RESIZE: clear(); getmaxyx(stdscr, size_y, size_x); line_buffer = realloc(line_buffer, size_x); break; case KEY_F(1): { task_t *new_task; if (create_task(db, &new_task) == false) { // ERROR break; } int row = db->selected_task; 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 (selected_task == NULL) { break; } // rename stuff int row = db->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 (selected_task == NULL) { break; } delete_task(db, selected_task); break; } case 'c': if (active_task != NULL) { db->selected_task = db->active_task; } break; case '\n': case ' ': { if (db != &database) { break; } task_t *next_task = selected_task; if (active_task != NULL) { update_timers(db); // TODO Should I keep this even though it always does? db->active_task = -1; } if (active_task != next_task) { db->active_task = next_task - db->tasks; } db->modified_on = time(NULL); store_database(db, DB_BIN_PATH_NAME); break; } case KEY_BACKSPACE: if (db == &database) { if (load_database(&archive, AR_BIN_PATH_NAME) == false) { store_database(&archive, AR_BIN_PATH_NAME); } db = &archive; } else { store_database(&archive, AR_BIN_PATH_NAME); reset_database(&archive); db = &database; } break; case KEY_LEFT: break; case KEY_RIGHT: break; case KEY_HOME: if (db->count > 0) { db->selected_task = 0; } break; case KEY_UP: if (db->selected_task > 0) { db->selected_task--; } break; case KEY_PPAGE: db->selected_task -= (size_y - 2); if (db->selected_task < 0) { db->selected_task = 0; } break; case KEY_END: if (db->count > 0) { db->selected_task = db->count - 1; } break; case KEY_DOWN: if (db->selected_task < db->count - 1) { db->selected_task++; } break; case KEY_NPAGE: db->selected_task += (size_y - 2); if (db->selected_task >= db->count) { db->selected_task = db->count - 1; } break; } if (size_x >= 60 && size_y > 2) { selected_layout = size_x > 100 ? 0 : 1; draw_tui(db); } else { const char *INVALID_WINDOW_MESSAGE = "Please expand window."; mvaddstr(size_y / 2, (size_x - strlen(INVALID_WINDOW_MESSAGE)) / 2, INVALID_WINDOW_MESSAGE); } } while((ch = getch()) != 'q'); update_timers(&database); store_database(&database, DB_BIN_PATH_NAME); if (db == &archive) { store_database(&archive, AR_BIN_PATH_NAME); } free_memory(); endwin(); return EXIT_SUCCESS; }