diff options
| author | dam <dam@gudinoff> | 2022-09-20 17:20:54 +0000 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2022-09-20 17:20:54 +0000 |
| commit | 98279a2d333e8ed14d036a9d5bd38200aa215b5a (patch) | |
| tree | eef8a5c9f396148956cdf2dca79c81c68309c715 | |
| parent | 9bbb7ce1b56814e6fa1668bf0ef21aaf602584a1 (diff) | |
| download | task-time-tracker-98279a2d333e8ed14d036a9d5bd38200aa215b5a.tar.zst task-time-tracker-98279a2d333e8ed14d036a9d5bd38200aa215b5a.zip | |
Let's say this is the first working prototype.
| -rw-r--r-- | main.c | 875 | ||||
| -rw-r--r-- | misc.c | 14 | ||||
| -rw-r--r-- | readme.md | 28 |
3 files changed, 514 insertions, 403 deletions
@@ -1,12 +1,13 @@ // Compilation command: -// - release: gcc main.c -Wall -O2 -m64 -lncurses -o ttt -// - debug : gcc main.c -Wall -g3 -m64 -lncurses -o ttt -D DEBUG +// - 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 <assert.h> #include <errno.h> #include <inttypes.h> #include <limits.h> +#include <locale.h> #include <ncurses.h> #include <stdbool.h> #include <stddef.h> @@ -14,39 +15,40 @@ #include <string.h> #include <time.h> -#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) +#define AR_BIN_PATH_NAME "./archive.bin" typedef struct /*__attribute__((__packed__))*/ { - uint32_t times[7]; + int64_t times[7]; char name[MAX_TASK_NAME]; } task_t; typedef struct /*__attribute__((__packed__))*/ { - time_t modified_on; + task_t *tasks; size_t count; size_t capacity; - task_t *tasks; ptrdiff_t active_task; - ptrdiff_t selected_task; // TODO Maybe use this instead of indexes? + ptrdiff_t selected_task; + int64_t modified_on; + int64_t total_times[7]; } database_t; -const char *DB_FILE_SIGN = "TTT:B:01"; -const size_t SIZEOF_DB_FILE_SIGN = sizeof(DB_FILE_SIGN); +#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; // TODO To be implemented in the future. +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. @@ -79,43 +81,107 @@ char *replace_char(char *string, char find, char replace) { 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); +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 > 60) { - double hours = (double)time / 3600.0; - double minutes = (time - (time_t)hours) / 60.0; - sprintf(string, "%02.0f:%02.0f", hours, minutes); + 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, "%2dsec", time); + 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 == DB_MAX_CAP) { + if (db->count == PTRDIFF_MAX) { fprintf(stderr, "Database reached maximum capacity.\n"); return false; } @@ -124,7 +190,7 @@ bool create_task(database_t *db, task_t **task) { 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 > PTRDIFF_MAX >> 1 ? PTRDIFF_MAX : current_capacity << 1; task_t *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_T); @@ -151,14 +217,44 @@ bool create_task(database_t *db, task_t **task) { 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); + memmove(task, task + 1, (db->count - index - 1) * SIZEOF_TASK_T); db->count--; // Adjust selected task. @@ -190,9 +286,12 @@ bool delete_task(database_t *db, task_t *task) { return true; } -void clear_database(database_t *db) { +// 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. @@ -209,7 +308,7 @@ bool store_database(const database_t *db, const char *path_name) { return false; } - fwrite(DB_FILE_SIGN, sizeof(char), SIZEOF_DB_FILE_SIGN, file); + 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); @@ -232,9 +331,9 @@ bool load_database(database_t *db, const char *path_name) { } // 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) { + 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; } @@ -284,7 +383,7 @@ bool export_to_csv(const database_t *db, const char *path_name) { 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", + fprintf(file, "%s,%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 ",%" PRId64 "\n", name, task->times[0], task->times[1], @@ -312,43 +411,35 @@ bool import_from_csv(database_t *db, const char *path_name) { 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); + 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. - name_delimiter = strchr(line_buffer, ','); + 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, - "%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32, + "%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64 ",%" SCNd64, &task->times[0], &task->times[1], &task->times[2], @@ -363,8 +454,11 @@ bool import_from_csv(database_t *db, const char *path_name) { continue; } - // Add new database entry. - number_of_entries++; + // 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); @@ -377,12 +471,8 @@ bool import_from_csv(database_t *db, const char *path_name) { 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_SOT = 0x02, + T_TPF = 0x04, T_ALL = 0xFF, }; @@ -401,86 +491,13 @@ void prototype(int level) { } /////////////////////////////////////////////////////////////////////////// - // 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) { + // Check size of types + if (level & T_SOT) { size_t size; char *name; - fprintf(stderr, "# check structs sizes ------------------------- \\\n"); + fprintf(stderr, "# check size of types ------------------------- \\\n"); + + fprintf(stderr, "sizeof(byte) = %u bits\n", CHAR_BIT); name = "database_t"; size = sizeof(database_t); @@ -494,9 +511,34 @@ void prototype(int level) { 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]); + } + } } @@ -510,9 +552,6 @@ void update_timers(database_t *db) { // 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; } @@ -523,11 +562,17 @@ void update_timers(database_t *db) { start_week_day = localtime(&start_time)->tm_wday; - // Get next week of day. - time_t next_day = (start_time / 86400) * 86400 + 86400; + // 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; - - active_task->times[start_week_day] += next_start - start_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; } @@ -535,6 +580,31 @@ void update_timers(database_t *db) { 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 (task_t *task = db->tasks; task < db->tasks + db->count; task++) { +// int64_t *times = task->times; + 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; @@ -544,10 +614,11 @@ uint8_t selected_layout = 0; #define NUM_OF_COLUMNS 9 typedef struct { - int columns_size; + 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; @@ -560,8 +631,6 @@ void initialize_tui() { // 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 = { @@ -580,8 +649,6 @@ void initialize_tui() { // 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 = { @@ -597,8 +664,31 @@ void initialize_tui() { }, }; + // 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. + cbreak(); // Line buffering disabled; pass on everty thing to me. keypad(stdscr, TRUE); // I need that nifty F1 curs_set(0); // Set cursor invisible. @@ -608,14 +698,8 @@ void initialize_tui() { 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); + reset_database(&database); free(line_buffer); line_buffer = NULL; @@ -625,25 +709,20 @@ void free_memory() { } -void draw_tui() { +void draw_tui(database_t *db) { + 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). + // 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]; } - clear(); - - + // TODO Unsure if this is needed. + erase(); // Draw outer border. box(stdscr, 0, 0); @@ -651,7 +730,6 @@ void draw_tui() { // 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++) { @@ -663,185 +741,160 @@ void draw_tui() { // 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; - 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; + idx_fdow = idx; + if (idx_fdow > 0 && idx_fdow < 8) { + idx_fdow = ((idx - 1) + FIRST_DAY_OF_WEEK) % 7 + 1; } - - mvaddstr(0, header_position, layout->table_headers[idx]); - x += layout->column_widths[idx]; + 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; - for (task_t *task = database.tasks; task < database.tasks + database.count; task++) { - y++; - int x = 0; - for (int idx = 0; idx < NUM_OF_COLUMNS; idx++) { - x++; - mvaddnstr(y, x, task->name, layout->column_widths[idx]); - break; // WIP TODO TASK BUG TEST HACK WARNING - } -// x += 1 + layouts->column_widths[0]; -// mvaddstr(y, x, - } - - - 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), - }; + int available_rows = size_y - 2; + task_t *active_task = get_active_task(db); + task_t *selected_task = get_selected_task(db); - 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)); - } + // 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++; -// 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)); + // 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)); } - } - - /////////////////////////////////////////////////////////////////////////// - // 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++){ + // TEST -- STOP - 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)); + int x = 0; - // Clear line_buffer and add string termination. - memset(line_buffer, ' ', size_x * sizeof(char)); - line_buffer[size_x-1] = '\0'; + // Task name. + x++; - // 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); + 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. - 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)); + x += layout->column_widths[0]; - // Update totals. - total = 0; - for(int idx = 7-1; idx >= 0; idx--) { - total += times[idx]; - totals[idx] += times[idx]; + // 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; } - 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. + // 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 - // 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); + // 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); } - // 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); + + // 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[]) { - // Make sure architecture uses 8bits per char. - assert(CHAR_BIT == 8); +// 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; - // TODO Parse commands using: https://stackoverflow.com/questions/9642732/parsing-command-line-arguments-in-c if (argc > 1) { char *action; @@ -849,94 +902,81 @@ int main(int argc, char *argv[]) { 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; + 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 = "--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; + do_action = strncmp(argv[idx], action, strlen(action)+1) == 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; + action = "--t_sot"; + do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { - prototype(T_ICSV); + prototype(T_SOT); return EXIT_SUCCESS; } - action = "--t_ecsv"; - do_action = strncmp(argv[idx], action, strlen(action)) == 0; + action = "--t_tpf"; + do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { - load_database(&database, DB_BIN_PATH_NAME); - prototype(T_ECSV); + prototype(T_TPF); return EXIT_SUCCESS; } - action = "--t_lbin"; - do_action = strncmp(argv[idx], action, strlen(action)) == 0; + action = "--test"; + do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { - prototype(T_LBIN); + prototype(T_ALL); return EXIT_SUCCESS; } - action = "--t_csv2bin"; - do_action = strncmp(argv[idx], action, strlen(action)) == 0; + action = "--icsv"; + do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { - clear_database(&database); - import_from_csv(&database, DB_CSV_PATH_NAME); + 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 = "--test"; - do_action = strncmp(argv[idx], action, strlen(action)) == 0; + action = "--ecsv"; + do_action = strncmp(argv[idx], action, strlen(action)+1) == 0; if (do_action) { - prototype(T_ALL); + 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; } - initialization(); - initialize_tui(); - int ch; - - + load_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. - timeout(10000); // Make getch() timeout after timeout(...) miliseconds. + // 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. - ch = KEY_RESIZE; + int ch = KEY_RESIZE; do { - task_t *active_task = database.tasks + database.active_task; - task_t *selected_task = database.tasks + database.selected_task; + task_t *active_task = get_active_task(&database); + task_t *selected_task = get_selected_task(&database); update_timers(&database); switch(ch) @@ -951,7 +991,7 @@ int main(int argc, char *argv[]) { // ERROR break; } - int row = database.count; + int row = database.selected_task; mvaddch(row, 0, ACS_DIAMOND); clrtoeol(); mvaddch(row, size_x-1, ACS_VLINE); @@ -980,7 +1020,7 @@ int main(int argc, char *argv[]) { case KEY_F(2): { - if (database.selected_task < 0) { + if (selected_task == NULL) { break; } // rename stuff @@ -996,34 +1036,43 @@ int main(int argc, char *argv[]) { case KEY_F(3): { - if (database.selected_task < 0) { + if (selected_task == NULL) { break; } delete_task(&database, selected_task); break; } + + case 'c': + if (active_task != NULL) { + db->selected_task = db->active_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); + { + task_t *next_task = selected_task; + if (active_task != NULL) { + 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(); + clear(); getmaxyx(stdscr, size_y, size_x); - line_buffer = realloc(line_buffer, size_x * sizeof(char)); + line_buffer = realloc(line_buffer, size_x); + break; + + case KEY_BACKSPACE: + db = db == &archive ? &database : &archive; break; case KEY_LEFT: @@ -1032,33 +1081,53 @@ int main(int argc, char *argv[]) { case KEY_RIGHT: break; + case KEY_HOME: + if (db->count > 0) { + db->selected_task = 0; + } + break; + case KEY_UP: if (database.selected_task > 0) { database.selected_task--; } break; + case KEY_PPAGE: + database.selected_task -= (size_y - 2); + if (database.selected_task < 0) { + database.selected_task = 0; + } + break; + + case KEY_END: + if (db->count > 0) { + db->selected_task = db->count - 1; + } + break; + case KEY_DOWN: if (database.selected_task < database.count - 1) { database.selected_task++; } - break; + break; + + case KEY_NPAGE: + database.selected_task += (size_y - 2); + if (database.selected_task >= database.count) { + database.selected_task = database.count - 1; + } + break; } if (size_x >= 60 && size_y > 2) { selected_layout = size_x > 100 ? 0 : 1; - draw_tui(); + 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); } - - // 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); @@ -1,3 +1,17 @@ +// Returns the number of characters in a string using UTF8 encoding. +size_t length_utf8(char *string) { + size_t size = 0; + char *ptr = string; + while(*ptr != '\0') { + if ((*ptr & 0xC0) != 0x80) { + size++; + } + ptr++; + } + return size; +} + + // Adds task to database. If necessary, expands database capacity. // Returns success. @@ -5,6 +5,10 @@ Task Time Tracker - Only one task may be active; - The log will be a circular array. During app startup the array will be loaded from a csv file. The log array should have a fixed length. Each string in the array should also have fixed length. Loading and storing it to a file will be implemented using readline and writeline operations. In order for the log file to be CSV compatible, we must always use the same format when writing to the log array. Best approach is to use a function that enfores the CSV format. The log should be written to a file every 5 minutes (not set in stone), if required; such may be done using a `log_is_dirty` flag. A possible structure for the log entries is: {uint64_t timestamp; uint8_t action[16]; uint8_t task_name[MAX_TASK_NAME+1]; uint8_t notes[16]; }. +# 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) # to-do list - [x] Include check on number of char bits; @@ -16,6 +20,30 @@ Task Time Tracker - [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; +- [ ] Don't store total_times on database_t: + - Create function to recalculate them. Shouldn't take so long, right? + - Decide when this will run. + - check for overflow/underflow when adding/subtracting times; +- [ ] Commit changes to repository. +- [ ] Adapt cycle to work with `database_t *db` to allow pointing to database/archive. +- [ ] How to show we're working with database or archive? + - When showing archive, change the 1st column name from "TTT v1" to "Archive". Maybe include this on the layout types? + - Maybe add a "app_mode" flag; +- [ ] Maybe rename database to something else that goes along with archive. +- [ ] Cleanup `draw_tui`: + - Selected and active tasks should be drawn after everything else. + - Try printing each row; + - Try printing headers and footers, then each row; + - Allow to repaint just certain parts of the TUI; this should allow to call `draw_tui` with a flag saying which parts need to be drawn; +- [ ] Allow to cancel a rename_task operation. +- [ ] Confirm delete_task operation. +- [ ] Implement logs as described above. +- [ ] Mouse selection is broken due to entire TUI update: + - By drawing only the lines that change, we can make the mouse selection not go away. + - Use DRAW_COMPONENT_FLAG to let draw_tui know what needs to be updated. - [ ] On compact layout, start task names right after the vertical line, otherwise, add a space (as is now). - [ ] Allow two view modes: one to iew non-archived tasks; another to view archived tasks. - [ ] To add a new task, create it and enter the task-name-edit mode. To edit, use the ACS_CKBOARD to show empty spaces and ACS_LARROW and ACS_RARROW to show continuation on horizontal direction. |
