aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordam <dam@gudinoff>2022-10-16 02:01:57 +0000
committerdam <dam@gudinoff>2022-10-16 02:01:57 +0000
commit14fac683284fe4725e1f68d069cbade0848fb4f3 (patch)
tree0bdf0e7a42696b665e1a1cfb4d53e6cce1cbed29
parentdd524b83197d85e04948634a1741a1bebe2907ef (diff)
downloadtask-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.c364
-rw-r--r--readme.md7
2 files changed, 273 insertions, 98 deletions
diff --git a/main.c b/main.c
index 3c0d7d7..ab8bef8 100644
--- a/main.c
+++ b/main.c
@@ -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);
diff --git a/readme.md b/readme.md
index f045c66..8a2b833 100644
--- a/readme.md
+++ b/readme.md
@@ -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`: