diff options
| author | dam <dam@gudinoff> | 2022-10-16 02:01:57 +0000 |
|---|---|---|
| committer | dam <dam@gudinoff> | 2022-10-16 02:01:57 +0000 |
| commit | 14fac683284fe4725e1f68d069cbade0848fb4f3 (patch) | |
| tree | 0bdf0e7a42696b665e1a1cfb4d53e6cce1cbed29 | |
| parent | dd524b83197d85e04948634a1741a1bebe2907ef (diff) | |
| download | task-time-tracker-14fac683284fe4725e1f68d069cbade0848fb4f3.tar.zst task-time-tracker-14fac683284fe4725e1f68d069cbade0848fb4f3.zip | |
Allow user to edit (set/add/remove) and reset the task times. Fixed bug when windows height was less than 3. Fixed bug when selecting next tasks using Arrow Down of Next Page.
| -rw-r--r-- | main.c | 364 | ||||
| -rw-r--r-- | readme.md | 7 |
2 files changed, 273 insertions, 98 deletions
@@ -2,7 +2,7 @@ // - release: gcc main.c -Wall -Werror -pedantic -O2 -m64 -lncursesw -o ttt // - debug : gcc main.c -Wall -Werror -pedantic -g3 -m64 -lncursesw -o ttt -D DEBUG // Usage hints: -// - To changes app data path change the environment variable HOME (USERPROFILE for windows users). +// - To change the app data path, overwride the environment variable HOME (USERPROFILE for windows users). #include <assert.h> @@ -36,10 +36,10 @@ typedef struct { typedef struct { task_st *tasks; - size_t count; - size_t capacity; - ptrdiff_t active_task; - ptrdiff_t selected_task; + size_t count; // Will always be equal or less than capacity. + size_t capacity; // Limited to PTRDIFF_MAX. + ptrdiff_t active_task; // Will always be less than capacity/count. + ptrdiff_t selected_task; // Will always be less than capacity/count. int64_t modified_on; int64_t total_times[NUM_WEEK_DAYS]; } database_st; @@ -286,7 +286,7 @@ bool add_task(database_st *db, task_st *task) { return true; } -// Deletes the task provided in the pointer. If possible, shrinks the database capacity. +// Deletes the provided task. If possible, shrinks the database capacity. // Returns success. bool delete_task(database_st *db, task_st *task) { assert(db != NULL); @@ -333,7 +333,7 @@ bool delete_task(database_st *db, task_st *task) { return true; } -// Deletes the task provided in the pointer. If possible, shrinks the database capacity. +// Deletes the provided task. If possible, shrinks the database capacity. // Returns success. bool move_task(database_st *db, task_st *task, size_t target) { assert(db != NULL); @@ -380,6 +380,102 @@ bool move_task(database_st *db, task_st *task, size_t target) { return true; // TODO } +// Updates the times on the active task (and adjusts database totals). +void update_times(database_st *db) { + assert(db != NULL); + + // Get current UTC time. + time_t stop_time = time(NULL); + + // Get last modified on UTC time. + time_t start_time = db->modified_on; + + if (db->active_task < 0) { + return; + } + + task_st *active_task = db->tasks + db->active_task; + uint8_t start_week_day; + while (start_time < stop_time) { + + start_week_day = localtime(&start_time)->tm_wday; + + // Get next day in local time. + struct tm *start_of_day_tm = localtime(&start_time); + start_of_day_tm->tm_sec = 0; + start_of_day_tm->tm_min = 0; + start_of_day_tm->tm_hour = 0; + time_t start_of_day = mktime(start_of_day_tm); + time_t next_day = start_of_day + SECONDS_IN_DAY; + time_t next_start = next_day < stop_time ? next_day : stop_time; + time_t elapsed_time = next_start - start_time; + active_task->times[start_week_day] += elapsed_time; + db->total_times[start_week_day] += elapsed_time; + + start_time = next_start; + } + + db->modified_on = stop_time; +} + +// Recalculates database totals. +void update_total_times(database_st *db) { + assert(db != NULL); + + 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, 0, NUM_WEEK_DAYS * sizeof(int64_t)); + + for (size_t idx = 0; idx < db->count; idx++) { + int64_t *times = db->tasks[idx].times; + *d0 = add_int64(*d0, times[0]); + *d1 = add_int64(*d1, times[1]); + *d2 = add_int64(*d2, times[2]); + *d3 = add_int64(*d3, times[3]); + *d4 = add_int64(*d4, times[4]); + *d5 = add_int64(*d5, times[5]); + *d6 = add_int64(*d6, times[6]); + } +} + +// Resets the times of the provided task (and adjusts database totals). +void reset_task_times(database_st *db, task_st *task) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task < &db->tasks[db->count]); + + // Make sure we sync before applying the changes. + update_times(db); + + for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) { + int64_t *timer = &task->times[idx]; + int64_t *total = &db->total_times[idx]; + *total = sub_int64(*total, *timer); + *timer = 0; + } +} + +// Sets the time on the day and task provided (and adjusts database totals). +void set_task_time(database_st *db, task_st *task, int day, int64_t time) { + assert(db != NULL); + assert(task != NULL); + assert(task >= db->tasks && task < &db->tasks[db->count]); + + // Make sure we sync before applying the changes. + update_times(db); + + int64_t *timer = &task->times[day]; + int64_t *total = &db->total_times[day]; + *total = sub_int64(*total, *timer); + *timer = time; + *total = add_int64(*total, *timer); +} + // Resets database to the initial state and deallocates all memory taken by tasks. void reset_database(database_st *db) { assert(db != NULL); @@ -561,6 +657,8 @@ bool import_from_csv(database_st *db, const char *path) { return true; } +// Appends task to the end of the CSV file. +// Returns success. bool append_to_csv(task_st *task, const char *path) { assert(task != NULL); assert(path != NULL); @@ -596,71 +694,12 @@ bool append_to_csv(task_st *task, const char *path) { return true; } -void update_timers(database_st *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_st *active_task = db->tasks + db->active_task; - uint8_t start_week_day; - while (start_time < stop_time) { - - start_week_day = localtime(&start_time)->tm_wday; - - // Get next day in local time. - struct tm *start_of_day_tm = localtime(&start_time); - start_of_day_tm->tm_sec = 0; - start_of_day_tm->tm_min = 0; - start_of_day_tm->tm_hour = 0; - time_t start_of_day = mktime(start_of_day_tm); - time_t next_day = start_of_day + SECONDS_IN_DAY; - time_t next_start = next_day < stop_time ? next_day : stop_time; - time_t elapsed_time = next_start - start_time; - active_task->times[start_week_day] += elapsed_time; - db->total_times[start_week_day] += elapsed_time; - - start_time = next_start; - } - - db->modified_on = stop_time; -} - -void update_total_timers(database_st *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, 0, NUM_WEEK_DAYS * sizeof(int64_t)); - - for (size_t idx = 0; idx < db->count; idx++) { - int64_t *times = db->tasks[idx].times; - *d0 = add_int64(*d0, times[0]); - *d1 = add_int64(*d1, times[1]); - *d2 = add_int64(*d2, times[2]); - *d3 = add_int64(*d3, times[3]); - *d4 = add_int64(*d4, times[4]); - *d5 = add_int64(*d5, times[5]); - *d6 = add_int64(*d6, times[6]); - } -} - #define INPUT_TIMEOUT_MS 1000 #define INPUT_AWAIT_INF -1 #define NUM_HEADER_ROWS 1 #define NUM_FOOTER_ROWS 1 -#define NUM_TABLE_ROWS (size_y - NUM_HEADER_ROWS - NUM_FOOTER_ROWS) #define NUM_COLUMNS 9 #define L_TITLE_IDX 0 @@ -691,7 +730,10 @@ typedef struct { char *archive_title; } layout_st; -layout_st layouts[NUM_LAYOUTS]; +layout_st layouts[NUM_LAYOUTS]; +int layout_tasks_rows; +bool is_valid_window = false; + void initialize_tui() { @@ -765,8 +807,11 @@ void initialize_tui() { init_pair(THEME_E, COLOR_BLUE, COLOR_BLACK); } -void recalculate_tui() { - // Recalculate first column width: expands to fill the remaining space dynamically. +void update_layout() { + // Calculate number of available rows to display tasks. + layout_tasks_rows = (size_y - NUM_HEADER_ROWS - NUM_FOOTER_ROWS); + + // Calculate first column width: expands to fill the remaining space dynamically. for (layout_st *layout = layouts; layout <= &layouts[NUM_LAYOUTS-1]; layout++) { layout->columns[0].width = size_x - (NUM_COLUMNS - 1) - 2; for (int idx = 1; idx < NUM_COLUMNS; idx++) { @@ -863,9 +908,8 @@ void draw_tui(database_st *db, layout_st *layout) { // TODO This is some sort of pagination to allow scrolling through the tasks. // TODO How does this behaves when no task is selected? y = 0; - size_t available_rows = NUM_TABLE_ROWS; - size_t idx_start = (db->selected_task / available_rows) * available_rows; - size_t idx_stop = idx_start + (available_rows > db->count - idx_start ? db->count - idx_start : available_rows); + size_t idx_start = (db->selected_task / layout_tasks_rows) * layout_tasks_rows; + size_t idx_stop = idx_start + (layout_tasks_rows > db->count - idx_start ? db->count - idx_start : layout_tasks_rows); for (size_t idx = idx_start; idx < idx_stop; idx++) { task_st *task = &db->tasks[idx]; y++; @@ -1084,14 +1128,14 @@ int main(int argc, char *argv[]) { ungetch(KEY_RESIZE); for (int key; (key = getch()) != 'q'; ) { - timeout(INPUT_AWAIT_INF); + static layout_st *layout = &layouts[L_COMPACT]; task_st *active_task = get_active_task(db); task_st *selected_task = get_selected_task(db); - int selected_task_row = (db->selected_task % NUM_TABLE_ROWS) + NUM_HEADER_ROWS; + int selected_task_row = is_valid_window ? (db->selected_task % layout_tasks_rows) + NUM_HEADER_ROWS : 0; int selected_task_theme = selected_task == active_task ? THEME_E : THEME_D; - layout_st *layout; - update_timers(&database); + timeout(INPUT_AWAIT_INF); + update_times(&database); switch(key) { @@ -1104,8 +1148,9 @@ int main(int argc, char *argv[]) { case KEY_RESIZE: { clear(); getmaxyx(stdscr, size_y, size_x); + is_valid_window = size_x >= 60 && size_y >= 3; string_buffer = realloc(string_buffer, 511 | MAX_TASK_NAME | (size_x + 1)); // TODO This realloc sucks. - recalculate_tui(); // TODO Maybe rename this function. + update_layout(); layout = &layouts[size_x > 100 ? L_NORMAL : L_COMPACT]; break; } @@ -1159,6 +1204,138 @@ int main(int argc, char *argv[]) { break; } + case '0': { + if (selected_task == NULL) { + break; + } + + attron(COLOR_PAIR(selected_task_theme) | A_BOLD); + + move(selected_task_row, 1); + for (int idx = 0; idx < size_x - 2; idx++) { + addch(ACS_CKBOARD); + } + mvaddstr(selected_task_row, 2, " Press enter to reset task. "); + + attrset(A_NORMAL); + + if (getch() == '\n') { + reset_task_times(db, selected_task); + } + + break; + } + + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': { + if (selected_task == NULL) { + break; + } + + int selected_day = key - '1'; + + attron(COLOR_PAIR(selected_task_theme) | A_BOLD); + + // Prepare row to input new task name. + int input_width = layout->columns[L_DAYS_IDX + selected_day].width; + int input_pos_x = 1 + layout->columns[L_TITLE_IDX].width; + for (int col = 0; col < selected_day; col++) { + input_pos_x += 1 + layout->columns[L_DAYS_IDX + col].width; + } + input_pos_x++; + + sprintf(string_buffer, "%*s", input_width, ""); + mvaddstr(selected_task_row, input_pos_x, string_buffer); + + // Get time delta. + echo(); + curs_set(1); + mvgetnstr(selected_task_row, input_pos_x, string_buffer, input_width); + noecho(); + curs_set(0); + + attrset(A_NORMAL); + + + // TODO Check if parsed OK. For that, I need to read the manual to know what strtoX returns. + // TODO It seems that the float parsing may return INF or NAN. Take special care with those. + // TODO Once I know the parse was OK, I'll check the remaining of the string for multiplies: + // s/S - second (default if none is found) + // m/M - minute + // h/H - hour + // d/D - day + // y/Y - year + char *input = string_buffer; + + if (is_empty_string(input) == true) { + break; + } + + char *assign_str = strchr(input, '='); + bool is_assign = assign_str != NULL; + if (is_assign == true) { + input = assign_str + 1; + } + + char *parser; + long double input_float = strtold(input, &parser); + long double multiplier = 1.0; + for (int i=0; i < strlen(parser); i++) { + char ch = parser[i]; + if (ch == 'm' || ch == 'M') { + multiplier = SECONDS_IN_MINUTE; + break; + } + else if (ch == 'h' || ch == 'H') { + multiplier = SECONDS_IN_HOUR; + break; + } + else if (ch == 'd' || ch == 'D') { + multiplier = SECONDS_IN_DAY; + break; + } + else if (ch == 'y' || ch == 'Y') { + multiplier = SECONDS_IN_YEAR; + break; + } + } + + long double result = input_float * multiplier; + int64_t seconds = result; + bool is_result_valid = (result >= (long double)INT64_MIN && result <= (long double)INT64_MAX); + char action = is_assign ? '=': result >= 0 ? '+' : '-'; + + // TODO TEST +// fprintf(stderr, "%c : %Lf x %Lf = %Lf\n", action, input_float, multiplier, result); +// fprintf(stderr, "[%20" PRId64 "\n", INT64_MIN); +// fprintf(stderr, " %20.0Lf\n", result); +// fprintf(stderr, " %20" PRId64 " is %s\n", seconds, is_result_valid ? "valid" : "INVALID"); +// fprintf(stderr, " %+20" PRId64 "]\n", INT64_MAX); + + if (is_result_valid == false) { + break; + } + + + // Make sure we sync before applying the changes. + update_times(db); + + int day = (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS; + int64_t time = selected_task->times[day]; + time = (action == '=' ? 0 : time) + seconds; + + // Adust time. + set_task_time(db, selected_task, day, time); + + + break; + } + case KEY_DC: { // Delete if (selected_task == NULL || selected_task == active_task) { break; @@ -1170,7 +1347,7 @@ int main(int argc, char *argv[]) { for (int idx = 0; idx < size_x - 2; idx++) { addch(ACS_CKBOARD); } - mvaddstr(selected_task_row, 2, " Press enter to delete. "); + mvaddstr(selected_task_row, 2, " Press enter to delete task. "); attrset(A_NORMAL); @@ -1198,7 +1375,7 @@ int main(int argc, char *argv[]) { // Get line number. echo(); curs_set(1); - mvgetnstr(selected_task_row, input_pos_x, string_buffer, MAX_TASK_NAME); // TODO use better value than MAX_TASK_NAME + mvgetnstr(selected_task_row, input_pos_x, string_buffer, size_x - input_pos_x - 1); noecho(); curs_set(0); @@ -1238,7 +1415,7 @@ int main(int argc, char *argv[]) { // Get line number. echo(); curs_set(1); - mvgetnstr(selected_task_row, input_pos_x, string_buffer, MAX_TASK_NAME); // TODO use better value than MAX_TASK_NAME + mvgetnstr(selected_task_row, input_pos_x, string_buffer, size_x - input_pos_x - 1); noecho(); curs_set(0); @@ -1269,7 +1446,7 @@ int main(int argc, char *argv[]) { } case KEY_F(5): { - update_total_timers(db); + update_total_times(db); break; } @@ -1288,7 +1465,7 @@ int main(int argc, char *argv[]) { } task_st *next_task = selected_task; if (active_task != NULL) { - update_timers(db); // TODO Should I keep this even though it always does? + update_times(db); // TODO Should I keep this even though it always does? db->active_task = -1; } if (active_task != next_task) { @@ -1335,12 +1512,6 @@ int main(int argc, char *argv[]) { break; } - case KEY_LEFT: - break; - - case KEY_RIGHT: - break; - case KEY_HOME: { if (db->count > 0) { db->selected_task = 0; @@ -1356,8 +1527,8 @@ int main(int argc, char *argv[]) { } case KEY_PPAGE: { - if (db->selected_task > NUM_TABLE_ROWS) { - db->selected_task -= NUM_TABLE_ROWS; + if (db->selected_task >= layout_tasks_rows) { + db->selected_task -= layout_tasks_rows; } else if (db->count > 0) { db->selected_task = 0; @@ -1373,15 +1544,15 @@ int main(int argc, char *argv[]) { } case KEY_DOWN: { - if (db->selected_task < db->count - 1) { + if (db->selected_task + 1 < db->count) { db->selected_task++; } break; } case KEY_NPAGE: { - if (db->selected_task < db->count - NUM_TABLE_ROWS) { - db->selected_task += NUM_TABLE_ROWS; + if (db->count >= layout_tasks_rows && db->selected_task < db->count - layout_tasks_rows) { + db->selected_task += layout_tasks_rows; } else if (db->count > 0) { db->selected_task = db->count - 1; @@ -1390,18 +1561,19 @@ int main(int argc, char *argv[]) { } } - if (size_x >= 60 && size_y >= 3) { + if (is_valid_window) { draw_tui(db, layout); } else { const char *INVALID_WINDOW_MESSAGE = "Please expand window."; - mvaddstr(size_y / 2, (size_x - strlen(INVALID_WINDOW_MESSAGE)) / 2, INVALID_WINDOW_MESSAGE); + const int INVALID_WINDOW_MESSAGE_LENGTH = strlen(INVALID_WINDOW_MESSAGE); + mvaddstr(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE_LENGTH) / 2, INVALID_WINDOW_MESSAGE); } timeout(INPUT_TIMEOUT_MS); } - update_timers(&database); + update_times(&database); store_database(&database, db_file_path); if (db == &archive) { export_to_csv(&archive, ar_file_path); @@ -52,8 +52,11 @@ Task Time Tracker - [x] Delete task using key: delete; - [x] Change task name using keys: `F2`; - [x] Duplicate task using keys: `d` and `D`; -- [ ] Add/remove time using keys: `F3`; -- [ ] Add/remove time for any day of week; +- [x] Add/remove time using keys: `F3`; +- [x] Add/remove time for any day of week; +- [x] Total times may saturate, but before that the user will see the infinite symbol. Solution: Provide user with possibility to refresh totals. +- [ ] After user input, we should save the changes on the database... or defer them and check every few seconds if we need to save. +- [ ] REVISE ALL CODE ptrdiff_t/size_t (signed/unsigned)! - [ ] Implement logs as described above. - [ ] Go over all `TODO` items; - [ ] Cleanup `draw_tui`: |
