aboutsummaryrefslogtreecommitdiff
path: root/ttt.c
diff options
context:
space:
mode:
Diffstat (limited to 'ttt.c')
-rw-r--r--ttt.c3170
1 files changed, 1583 insertions, 1587 deletions
diff --git a/ttt.c b/ttt.c
index 65d1144..0ec4355 100644
--- a/ttt.c
+++ b/ttt.c
@@ -1,24 +1,20 @@
// Compilation command:
-// - release dynamics libs : gcc main.c -lncursesw -ltinfo -o ttt -Wall -Werror -pedantic -O2 -m64 -s
-// - release static libs : gcc main.c -lncursesw -ltinfo -o ttt -Wall -Werror -pedantic -O2 -m64 -s -static-pie
-// - debug : gcc main.c -lncursesw -ltinfo -o ttt -Wall -Werror -pedantic -g3 -m64
+// - release dynamics libs : gcc ttt.c -lncursesw -ltinfo -o ttt -Wall -Werror -pedantic -O2 -m64 -s
+// - release static libs : gcc ttt.c -lncursesw -ltinfo -o ttt -Wall -Werror -pedantic -O2 -m64 -s -static-pie
+// - debug : gcc ttt.c -lncursesw -ltinfo -o ttt -Wall -Werror -pedantic -g3 -m64
//
// Compiler flags:
-// -l : libraries to link
-// -o : output file name
-// -Wall : enables all compiler's warning messages
-// -Werror : make all warnings into errors
-// -pedantic : issue all the warnings demanded by strict ISO C
-// -O : code optimization level (commonly accepted as best: 2)
-// -g : debug information level (max: 3)
-// -m64 : 64b architecture
-// -D : defines for preprocessor
-// -static-pie : link statically producing an position-independent executable
-// -DNDEBUG : remove assertions from code (not used)
-//
-// Usage hints:
-// - To change the app data path, overwride the environment variable HOME (USERPROFILE for windows users).
-
+// -l : libraries to link
+// -o : output file name
+// -Wall : enables all compiler's warning messages
+// -Werror : make all warnings into errors
+// -pedantic : issue all the warnings demanded by strict ISO C
+// -O : code optimization level (commonly accepted as best: 2)
+// -g : debug information level (max: 3)
+// -m64 : 64b architecture
+// -D : defines for preprocessor
+// -static-pie : link statically producing an position-independent executable
+// -DNDEBUG : remove assertions from code (not used)
#include <assert.h>
#include <errno.h>
@@ -37,70 +33,70 @@
#include <sys/types.h>
#include <time.h>
-#define VERSION "1.0" // Use only 3 chars (to fit layouts).
-#define TASK_NAME_LENGTH 57 // Task name length.
-#define TASK_NAME_BYTES (TASK_NAME_LENGTH+1)
-#define FIRST_DAY_OF_WEEK 1 // (0-6, Sunday = 0).
-#define NUM_WEEK_DAYS 7 // Just to avoid magic numbers.
+#define VERSION "1.0" // Use only 3 chars (to fit layouts).
+#define TASK_NAME_LENGTH 57 // Task name length.
+#define TASK_NAME_BYTES (TASK_NAME_LENGTH+1)
+#define FIRST_DAY_OF_WEEK 1 // (0-6, Sunday = 0).
+#define NUM_WEEK_DAYS 7 // Just to avoid magic numbers.
#if defined(_WIN64)
-#define HOME_PATH_ENV "USERPROFILE"
+#define HOME_PATH_ENV "USERPROFILE"
#else
-#define HOME_PATH_ENV "HOME"
+#define HOME_PATH_ENV "HOME"
#endif
-#define APP_FOLDER_NAME ".task_time_tracker"
-#define DB_FILE_NAME "database.bin"
-#define AR_FILE_NAME "archive.csv"
+#define APP_FOLDER_NAME ".task_time_tracker"
+#define DB_FILE_NAME "database.bin"
+#define AR_FILE_NAME "archive.csv"
typedef struct {
- int64_t times[NUM_WEEK_DAYS];
- char name[TASK_NAME_BYTES];
+ int64_t times[NUM_WEEK_DAYS];
+ char name[TASK_NAME_BYTES];
} task_st;
typedef struct {
- task_st *tasks;
- size_t count; // Will always be equal or less than capacity.
- size_t capacity; // Will always be equal or less than PTRDIFF_MAX (see MAX_DATABASE_TASKS).
- 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];
+ task_st *tasks;
+ size_t count; // Will always be equal or less than capacity.
+ size_t capacity; // Will always be equal or less than PTRDIFF_MAX (see MAX_DATABASE_TASKS).
+ 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;
-#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_ST = sizeof(task_st);
-const size_t SIZEOF_DATABASE_ST = sizeof(database_st);
-const size_t SIZEOF_CHAR = sizeof(char);
-const size_t SIZEOF_INT64 = sizeof(int64_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;
-const size_t MAX_DATABASE_TASKS = (PTRDIFF_MAX < (SIZE_MAX / SIZEOF_TASK_ST)) ? PTRDIFF_MAX : (SIZE_MAX / SIZEOF_TASK_ST);
-
-
-database_st database = { .tasks = NULL };
-database_st archive = { .tasks = NULL };
-database_st *db = NULL;
-bool is_autosave_enabled = true;
-int countdown_to_autosave = -1;
-char *app_folder = NULL;
-char *db_file_path = NULL;
-char *ar_file_path = NULL;
-char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors.
-size_t string_buffer_size = 0;
+#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_ST = sizeof(task_st);
+const size_t SIZEOF_DATABASE_ST = sizeof(database_st);
+const size_t SIZEOF_CHAR = sizeof(char);
+const size_t SIZEOF_INT64 = sizeof(int64_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;
+const size_t MAX_DATABASE_TASKS = (PTRDIFF_MAX < (SIZE_MAX / SIZEOF_TASK_ST)) ? PTRDIFF_MAX : (SIZE_MAX / SIZEOF_TASK_ST);
+
+
+database_st database = { .tasks = NULL };
+database_st archive = { .tasks = NULL };
+database_st *db = NULL;
+bool is_autosave_enabled = true;
+int countdown_to_autosave = -1;
+char *app_folder = NULL;
+char *db_file_path = NULL;
+char *ar_file_path = NULL;
+char *string_buffer = NULL; // A temporary buffer for localized actions. Please avoid data leaks and out-of-bounds errors.
+size_t string_buffer_size = 0;
int size_x, size_y, pos_x, pos_y;
typedef enum {
- STYLE_SELECTED = 1,
+ STYLE_SELECTED = 1,
STYLE_SELECTED_INVERTED,
STYLE_ACTIVE,
- STYLE_ACTIVE_SELECTED,
- STYLE_ERROR,
+ STYLE_ACTIVE_SELECTED,
+ STYLE_ERROR,
} styles_et;
@@ -109,85 +105,85 @@ WINDOW *error_window = NULL;
time_t error_time_limit = 0;
void print_error(const char *format, ...) {
- va_list args;
- va_start(args, format);
- if (stdscr == NULL || isendwin() == true) { // Not in ncurses mode.
- vfprintf(stderr, format, args);
- fprintf(stderr, "\n");
- }
- else {
- int w_size_x = size_x > 120 ? 120 : size_x - 2;
- int w_size_y = 4;
- if (error_window == NULL) {
- error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2);
- wattron(error_window, COLOR_PAIR(STYLE_ERROR));
- wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE);
- mvwprintw(error_window, 0, 1, " Error ");
- wmove(error_window, 1, 0);
- }
- else {
- waddch(error_window, ' ');
- }
- vw_printw(error_window, format, args);
- error_time_limit = time(NULL) + 5;
- }
-
- va_end(args);
+ va_list args;
+ va_start(args, format);
+ if (stdscr == NULL || isendwin() == true) { // Not in ncurses mode.
+ vfprintf(stderr, format, args);
+ fprintf(stderr, "\n");
+ }
+ else {
+ int w_size_x = size_x > 120 ? 120 : size_x - 2;
+ int w_size_y = 4;
+ if (error_window == NULL) {
+ error_window = newwin(w_size_y, w_size_x, (size_y - w_size_y) / 2, (size_x - w_size_x) / 2);
+ wattron(error_window, COLOR_PAIR(STYLE_ERROR));
+ wborder(error_window, ' ', ' ', 0, 0, ACS_HLINE, ACS_HLINE, ACS_HLINE, ACS_HLINE);
+ mvwprintw(error_window, 0, 1, " Error ");
+ wmove(error_window, 1, 0);
+ }
+ else {
+ waddch(error_window, ' ');
+ }
+ vw_printw(error_window, format, args);
+ error_time_limit = time(NULL) + 5;
+ }
+
+ va_end(args);
}
void draw_error_window() {
- if (error_window == NULL) {
- return;
- }
-
- // Hide error window after time-limit or if terminal is shrank.
- int w_size_x, w_size_y;
- getmaxyx(error_window, w_size_y, w_size_x);
- if (time(NULL) >= error_time_limit
- || size_x - w_size_x < 2
- || size_y - w_size_y < 2
- ) {
- delwin(error_window);
- error_window = NULL;
- return;
- }
-
- // Adjust error window position.
- int pos_x = (size_x - w_size_x) / 2;
- int pos_y = (size_y - w_size_y) / 2;
- mvwin(error_window, pos_y, pos_x);
-
- // Avoid being overwritten by main window content.
- refresh();
- touchwin(error_window);
- wrefresh(error_window);
+ if (error_window == NULL) {
+ return;
+ }
+
+ // Hide error window after time-limit or if terminal is shrank.
+ int w_size_x, w_size_y;
+ getmaxyx(error_window, w_size_y, w_size_x);
+ if (time(NULL) >= error_time_limit
+ || size_x - w_size_x < 2
+ || size_y - w_size_y < 2
+ ) {
+ delwin(error_window);
+ error_window = NULL;
+ return;
+ }
+
+ // Adjust error window position.
+ int pos_x = (size_x - w_size_x) / 2;
+ int pos_y = (size_y - w_size_y) / 2;
+ mvwin(error_window, pos_y, pos_x);
+
+ // Avoid being overwritten by main window content.
+ refresh();
+ touchwin(error_window);
+ wrefresh(error_window);
}
void trigger_autosave() {
- countdown_to_autosave = 13375; // ms
+ countdown_to_autosave = 13375; // ms
}
void show_processing() {
- mvaddch(0, 0, ACS_DIAMOND);
- refresh();
+ mvaddch(0, 0, ACS_DIAMOND);
+ refresh();
}
// Checks if file is exists and is accessible.
// Returns true when the file exists and is accessible.
bool is_file_accessible(const char *path) {
- assert(path != NULL);
- FILE *file = fopen(path, "r+");
- bool is_file_accessible = file != NULL;
- if (is_file_accessible) {
- fclose(file);
- }
- return is_file_accessible;
+ assert(path != NULL);
+ FILE *file = fopen(path, "r+");
+ bool is_file_accessible = file != NULL;
+ if (is_file_accessible) {
+ fclose(file);
+ }
+ return is_file_accessible;
}
// Returns true if string to_compare is equal to any of the other passed strings, false otherwise.
bool is_equal_to_any(const char *to_compare, const char *test_a, const char *test_b) {
- return strncmp(to_compare, test_a, strlen(test_a)+1) == 0
- || strncmp(to_compare, test_b, strlen(test_b)+1) == 0;
+ return strncmp(to_compare, test_a, strlen(test_a)+1) == 0
+ || strncmp(to_compare, test_b, strlen(test_b)+1) == 0;
}
// Given an UTF8 encoded string, truncate it to length bytes without breaking any UTF8 character.
@@ -195,59 +191,59 @@ bool is_equal_to_any(const char *to_compare, const char *test_a, const char *tes
// The terminating null byte ('\0') is not included in length.
// Returns the truncated string length.
size_t truncate_string_utf8(char *string, size_t length) {
- assert(string != NULL);
-
- // Find index of first continuation byte.
- size_t idx = length;
- while (idx > 0 && ((string[idx - 1] & 0xC0) == 0x80)) {
- idx--;
- }
- size_t continuation_bytes = length - idx;
-
- // If string starts with continuation bytes, it's an invalid UTF8 string.
- if (idx == 0 && continuation_bytes > 0) {
- length = 0;
- }
- // If length truncates some continuation bytes, remove incomplete UTF8 character.
- else if (idx > 0 // string is not empty
- // continuation bytes are not complete
- && !(continuation_bytes == 0 && (string[idx - 1] & 0x80) == 0x00)
- && !(continuation_bytes == 1 && (string[idx - 1] & 0xE0) == 0xC0)
- && !(continuation_bytes == 2 && (string[idx - 1] & 0xF0) == 0xE0)
- && !(continuation_bytes == 3 && (string[idx - 1] & 0xF8) == 0xF0)
- ) {
- length -= (continuation_bytes + 1); // Remove '+ 1' start byte.
- }
-
- string[length] = '\0';
- return length;
+ assert(string != NULL);
+
+ // Find index of first continuation byte.
+ size_t idx = length;
+ while (idx > 0 && ((string[idx - 1] & 0xC0) == 0x80)) {
+ idx--;
+ }
+ size_t continuation_bytes = length - idx;
+
+ // If string starts with continuation bytes, it's an invalid UTF8 string.
+ if (idx == 0 && continuation_bytes > 0) {
+ length = 0;
+ }
+ // If length truncates some continuation bytes, remove incomplete UTF8 character.
+ else if (idx > 0 // string is not empty
+ // continuation bytes are not complete
+ && !(continuation_bytes == 0 && (string[idx - 1] & 0x80) == 0x00)
+ && !(continuation_bytes == 1 && (string[idx - 1] & 0xE0) == 0xC0)
+ && !(continuation_bytes == 2 && (string[idx - 1] & 0xF0) == 0xE0)
+ && !(continuation_bytes == 3 && (string[idx - 1] & 0xF8) == 0xF0)
+ ) {
+ length -= (continuation_bytes + 1); // Remove '+ 1' start byte.
+ }
+
+ string[length] = '\0';
+ return length;
}
// Returns true when the string is empty or consists of white space characters.
bool is_empty_string(const char *string) {
- for (int idx = 0; string[idx] != '\0'; idx++) {
- switch(string[idx]) {
- case ' ':
- case '\t':
- case '\v':
- case '\f':
- case '\r':
- case '\n':
- break;
- default:
- return false;
- }
- }
- return true;
+ for (int idx = 0; string[idx] != '\0'; idx++) {
+ switch(string[idx]) {
+ case ' ':
+ case '\t':
+ case '\v':
+ case '\f':
+ case '\r':
+ case '\n':
+ break;
+ default:
+ return false;
+ }
+ }
+ return true;
}
// 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++;
+ char *idx = string;
+ while((idx = strchr(idx, find)) != NULL) {
+ *idx = replace;
+ idx++;
}
return string;
}
@@ -255,1587 +251,1587 @@ char *replace_char(char *string, char find, char replace) {
// Prints, on row y and column x, the time using 5 characters centered on space.
// Returns the result of a call to mvprintw.
int mvprintw_time(int y, int x, intmax_t time, int space) {
- const int TIME_CHARS = 5;
- assert(space >= TIME_CHARS);
-
- int left_padding = (space - TIME_CHARS) / 2;
- int right_padding = space - TIME_CHARS - left_padding;
-
- if (time < 0) {
- return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, "");
- }
- else if (time == 0) {
- return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, "");
- }
- else if (time < SECONDS_IN_MINUTE) {
- return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, "");
- }
- else if (time < (intmax_t)100 * SECONDS_IN_HOUR) {
- intmax_t hours = (double)time / (double)SECONDS_IN_HOUR;
- intmax_t minutes = (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE;
- return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, "");
- }
- else if (time < (intmax_t)(9999.5 * SECONDS_IN_DAY)) {
- double value = (double)time / (double)SECONDS_IN_DAY;
- int decimals =
- time >= 99.95 * SECONDS_IN_DAY ? 0 :
- time >= 9.995 * SECONDS_IN_DAY ? 1 :
- 2;
- return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, "");
- }
- else if (time < (intmax_t)(9999.5 * SECONDS_IN_YEAR)) {
- double value = (double)time / (double)SECONDS_IN_YEAR;
- int decimals =
- time >= 99.95 * SECONDS_IN_YEAR ? 0 :
- time >= 9.995 * SECONDS_IN_YEAR ? 1 :
- 2;
- return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, "");
- }
- else {
- return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, "");
- }
+ const int TIME_CHARS = 5;
+ assert(space >= TIME_CHARS);
+
+ int left_padding = (space - TIME_CHARS) / 2;
+ int right_padding = space - TIME_CHARS - left_padding;
+
+ if (time < 0) {
+ return mvprintw(y, x, "%*s - %*s", left_padding, "", right_padding, "");
+ }
+ else if (time == 0) {
+ return mvprintw(y, x, "%*s 0 %*s", left_padding, "", right_padding, "");
+ }
+ else if (time < SECONDS_IN_MINUTE) {
+ return mvprintw(y, x, "%*s%3jds %*s", left_padding, "", time, right_padding, "");
+ }
+ else if (time < (intmax_t)100 * SECONDS_IN_HOUR) {
+ intmax_t hours = (double)time / (double)SECONDS_IN_HOUR;
+ intmax_t minutes = (time - (hours * SECONDS_IN_HOUR) ) / SECONDS_IN_MINUTE;
+ return mvprintw(y, x, "%*s%02jd:%02jd%*s", left_padding, "", hours, minutes, right_padding, "");
+ }
+ else if (time < (intmax_t)(9999.5 * SECONDS_IN_DAY)) {
+ double value = (double)time / (double)SECONDS_IN_DAY;
+ int decimals =
+ time >= 99.95 * SECONDS_IN_DAY ? 0 :
+ time >= 9.995 * SECONDS_IN_DAY ? 1 :
+ 2;
+ return mvprintw(y, x, "%*s%4.*fd%*s", left_padding, "", decimals, value, right_padding, "");
+ }
+ else if (time < (intmax_t)(9999.5 * SECONDS_IN_YEAR)) {
+ double value = (double)time / (double)SECONDS_IN_YEAR;
+ int decimals =
+ time >= 99.95 * SECONDS_IN_YEAR ? 0 :
+ time >= 9.995 * SECONDS_IN_YEAR ? 1 :
+ 2;
+ return mvprintw(y, x, "%*s%4.*fy%*s", left_padding, "", decimals, value, right_padding, "");
+ }
+ else {
+ return mvprintw(y, x, "%*s ∞ %*s", left_padding, "", right_padding, "");
+ }
}
int64_t add_int64(int64_t x, int64_t y) {
- int64_t result;
+ int64_t result;
#ifdef __GNUC__
- bool overflow = __builtin_add_overflow(x, y, &result);
- if (overflow) {
- result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN)
- }
+ bool overflow = __builtin_add_overflow(x, y, &result);
+ if (overflow) {
+ result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN)
+ }
#else
- result =
- (y > 0 && x > INT64_MAX - y) ? INT64_MAX :
- (y < 0 && x < INT64_MIN - y) ? INT64_MIN :
- x + y;
+ result =
+ (y > 0 && x > INT64_MAX - y) ? INT64_MAX :
+ (y < 0 && x < INT64_MIN - y) ? INT64_MIN :
+ x + y;
#endif
- return result;
+ return result;
}
int64_t sub_int64(int64_t x, int64_t y) {
- int64_t result;
+ int64_t result;
#ifdef __GNUC__
- bool overflow = __builtin_sub_overflow(x, y, &result);
- if (overflow) {
- result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN)
- }
+ bool overflow = __builtin_sub_overflow(x, y, &result);
+ if (overflow) {
+ result = ((uint64_t)x >> 63) + INT64_MAX; // Equivalent to (x > 0 ? INT64_MAX : INT64_MIN)
+ }
#else
- result =
- (y < 0 && x > INT64_MAX + y) ? INT64_MAX :
- (y > 0 && x < INT64_MIN + y) ? INT64_MIN :
- x - y;
+ result =
+ (y < 0 && x > INT64_MAX + y) ? INT64_MAX :
+ (y > 0 && x < INT64_MIN + y) ? INT64_MIN :
+ x - y;
#endif
- return result;
+ return result;
}
// Returns active task or NULL if none applies.
task_st *get_active_task(database_st *db) {
- assert(db != NULL);
-
- task_st *task = NULL;
- if (db->active_task >= 0) {
- task = db->tasks + db->active_task;
- }
- return task;
+ assert(db != NULL);
+
+ task_st *task = NULL;
+ if (db->active_task >= 0) {
+ task = db->tasks + db->active_task;
+ }
+ return task;
}
// Returns selected task or NULL if none applies.
task_st *get_selected_task(database_st *db) {
- assert(db != NULL);
-
- task_st *task = NULL;
- if (db->selected_task >= 0) {
- task = db->tasks + db->selected_task;
- }
- return task;
+ assert(db != NULL);
+
+ task_st *task = NULL;
+ if (db->selected_task >= 0) {
+ task = db->tasks + db->selected_task;
+ }
+ return task;
}
// Creates new task stored at location given by task pointer.
// If necessary, expands database capacity.
// Returns success.
bool create_task(database_st *db, task_st **task) {
- assert(db != NULL);
- assert(task != NULL);
-
- if (db->count >= MAX_DATABASE_TASKS) {
- print_error("Database reached maximum capacity.");
- 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 > (MAX_DATABASE_TASKS >> 1) ? MAX_DATABASE_TASKS :
- current_capacity << 1;
-
- task_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST);
- if (new_tasks == NULL) {
- print_error("Failed to expand database.");
- return false;
- }
-
- db->capacity = new_capacity;
- db->tasks = new_tasks;
- }
-
- // Prepare new task.
- *task = &db->tasks[db->count];
- memset(*task, 0, SIZEOF_TASK_ST);
-
- db->count++;
-
- // Adjust selected task.
- if (db->selected_task < 0) {
- db->selected_task = db->count-1;
- }
-
- return true;
+ assert(db != NULL);
+ assert(task != NULL);
+
+ if (db->count >= MAX_DATABASE_TASKS) {
+ print_error("Database reached maximum capacity.");
+ 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 > (MAX_DATABASE_TASKS >> 1) ? MAX_DATABASE_TASKS :
+ current_capacity << 1;
+
+ task_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST);
+ if (new_tasks == NULL) {
+ print_error("Failed to expand database.");
+ return false;
+ }
+
+ db->capacity = new_capacity;
+ db->tasks = new_tasks;
+ }
+
+ // Prepare new task.
+ *task = &db->tasks[db->count];
+ memset(*task, 0, SIZEOF_TASK_ST);
+
+ db->count++;
+
+ // Adjust selected task.
+ if (db->selected_task < 0) {
+ db->selected_task = db->count-1;
+ }
+
+ return true;
}
// Duplicates the given task. Duplicated task is appended to the database.
// Returns success.
bool duplicate_task(database_st *db, task_st *task) {
- assert(db != NULL);
- assert(task != NULL);
-
- // Create new task and keep task_idx (relative pointer) of original task).
- ptrdiff_t task_idx = task - db->tasks;
- task_st *new_task;
- if (create_task(db, &new_task) == false) {
- return false;
- }
-
- // If original task belonged to database, fix its pointer.
- if (0 <= task_idx && task_idx < db->count - 1) { // Compensate '- 1' for the new task.
- task = db->tasks + task_idx;
- }
-
- memcpy(new_task, task, SIZEOF_TASK_ST);
-
- // Add task time values to total times.
- for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) {
- db->total_times[idx] = add_int64(db->total_times[idx], new_task->times[idx]);
- }
-
- return true;
+ assert(db != NULL);
+ assert(task != NULL);
+
+ // Create new task and keep task_idx (relative pointer) of original task).
+ ptrdiff_t task_idx = task - db->tasks;
+ task_st *new_task;
+ if (create_task(db, &new_task) == false) {
+ return false;
+ }
+
+ // If original task belonged to database, fix its pointer.
+ if (0 <= task_idx && task_idx < db->count - 1) { // Compensate '- 1' for the new task.
+ task = db->tasks + task_idx;
+ }
+
+ memcpy(new_task, task, SIZEOF_TASK_ST);
+
+ // Add task time values to total times.
+ for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) {
+ db->total_times[idx] = add_int64(db->total_times[idx], new_task->times[idx]);
+ }
+
+ return true;
}
// Deletes task from database.
// If possible, shrinks the database capacity.
// Returns success.
bool delete_task(database_st *db, task_st *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 < NUM_WEEK_DAYS; idx++) {
- db->total_times[idx] = sub_int64(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_ST);
- 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_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST);
- if (new_tasks == NULL && new_capacity > 0) {
- print_error("Failed to shrink database.");
- return false;
- }
- db->capacity = new_capacity;
- db->tasks = new_tasks;
- }
-
- return true;
+ 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 < NUM_WEEK_DAYS; idx++) {
+ db->total_times[idx] = sub_int64(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_ST);
+ 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_st *new_tasks = realloc(db->tasks, new_capacity * SIZEOF_TASK_ST);
+ if (new_tasks == NULL && new_capacity > 0) {
+ print_error("Failed to shrink database.");
+ return false;
+ }
+ db->capacity = new_capacity;
+ db->tasks = new_tasks;
+ }
+
+ return true;
}
// Moves task to index.
// Index gets clamped to [0, db->count[.
void move_task_to_index(database_st *db, task_st *task, ptrdiff_t index) {
- assert(db != NULL);
- assert(task != NULL);
- assert(task >= db->tasks && task - db->tasks < db->count);
-
- ptrdiff_t target_index = index < 0 ? 0
- : index >= db->count ? db->count - 1
- : index;
-
- task_st *target_task = db->tasks + target_index;
-
- if (target_task == task) {
- return;
- }
-
- // Move task to new location.
- task_st temp_task;
- memcpy(&temp_task, task, SIZEOF_TASK_ST);
- if (target_task > task) {
- memmove(task, task + 1, (target_task - task) * SIZEOF_TASK_ST);
- }
- else {
- memmove(target_task + 1, target_task, (task - target_task) * SIZEOF_TASK_ST);
- }
- memcpy(target_task, &temp_task, SIZEOF_TASK_ST);
-
- // Adjust active and selected tasks.
- ptrdiff_t source_index = task - db->tasks;
- if (db->active_task == source_index) {
- db->active_task = target_index;
- }
- else if (source_index < db->active_task && db->active_task <= target_index) {
- db->active_task--;
- }
- else if (target_index <= db->active_task && db->active_task < source_index) {
- db->active_task++;
- }
- db->selected_task = target_index;
+ assert(db != NULL);
+ assert(task != NULL);
+ assert(task >= db->tasks && task - db->tasks < db->count);
+
+ ptrdiff_t target_index = index < 0 ? 0
+ : index >= db->count ? db->count - 1
+ : index;
+
+ task_st *target_task = db->tasks + target_index;
+
+ if (target_task == task) {
+ return;
+ }
+
+ // Move task to new location.
+ task_st temp_task;
+ memcpy(&temp_task, task, SIZEOF_TASK_ST);
+ if (target_task > task) {
+ memmove(task, task + 1, (target_task - task) * SIZEOF_TASK_ST);
+ }
+ else {
+ memmove(target_task + 1, target_task, (task - target_task) * SIZEOF_TASK_ST);
+ }
+ memcpy(target_task, &temp_task, SIZEOF_TASK_ST);
+
+ // Adjust active and selected tasks.
+ ptrdiff_t source_index = task - db->tasks;
+ if (db->active_task == source_index) {
+ db->active_task = target_index;
+ }
+ else if (source_index < db->active_task && db->active_task <= target_index) {
+ db->active_task--;
+ }
+ else if (target_index <= db->active_task && db->active_task < source_index) {
+ db->active_task++;
+ }
+ db->selected_task = target_index;
}
// 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;
-
- // Keep track of this update.
- db->modified_on = stop_time;
-
- 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;
- }
+ 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;
+
+ // Keep track of this update.
+ db->modified_on = stop_time;
+
+ 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;
+ }
}
// Recalculates database totals.
void update_total_times(database_st *db) {
- assert(db != NULL);
-
- int64_t *totals = db->total_times;
- memset(totals, 0, NUM_WEEK_DAYS * SIZEOF_INT64);
- for (size_t idx = 0; idx < db->count; idx++) {
- int64_t *times = db->tasks[idx].times;
- totals[0] = add_int64(totals[0], times[0]);
- totals[1] = add_int64(totals[1], times[1]);
- totals[2] = add_int64(totals[2], times[2]);
- totals[3] = add_int64(totals[3], times[3]);
- totals[4] = add_int64(totals[4], times[4]);
- totals[5] = add_int64(totals[5], times[5]);
- totals[6] = add_int64(totals[6], times[6]);
- }
+ assert(db != NULL);
+
+ int64_t *totals = db->total_times;
+ memset(totals, 0, NUM_WEEK_DAYS * SIZEOF_INT64);
+ for (size_t idx = 0; idx < db->count; idx++) {
+ int64_t *times = db->tasks[idx].times;
+ totals[0] = add_int64(totals[0], times[0]);
+ totals[1] = add_int64(totals[1], times[1]);
+ totals[2] = add_int64(totals[2], times[2]);
+ totals[3] = add_int64(totals[3], times[3]);
+ totals[4] = add_int64(totals[4], times[4]);
+ totals[5] = add_int64(totals[5], times[5]);
+ totals[6] = add_int64(totals[6], 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;
- }
+ 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);
+ 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);
}
// Adds the time on the day and task provided (and adjusts database totals).
void add_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);
-
- task->times[day] = add_int64(task->times[day], time);
- db->total_times[day] = add_int64(db->total_times[day], 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);
+
+ task->times[day] = add_int64(task->times[day], time);
+ db->total_times[day] = add_int64(db->total_times[day], time);
}
// Resets database to the initial state and deallocates all memory taken by tasks.
void reset_database(database_st *db) {
- assert(db != NULL);
-
- free(db->tasks);
- memset(db, 0, SIZEOF_DATABASE_ST);
- db->active_task = -1;
- db->selected_task = -1;
+ assert(db != NULL);
+
+ free(db->tasks);
+ memset(db, 0, SIZEOF_DATABASE_ST);
+ db->active_task = -1;
+ db->selected_task = -1;
}
// Stores data from database into binary file.
// Returns success.
bool store_database(const database_st *db, const char *path) {
- assert(db != NULL);
- assert(path != NULL);
-
- // Open file.
- FILE *file = fopen(path, "wb");
- if (file == NULL) {
- print_error("Failed to open file '%s' while storing database: %s.", path, strerror(errno));
- return false;
- }
-
- fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file);
- fwrite(db, SIZEOF_DATABASE_ST, 1, file);
- fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file);
-
- fclose(file);
- return true;
+ assert(db != NULL);
+ assert(path != NULL);
+
+ // Open file.
+ FILE *file = fopen(path, "wb");
+ if (file == NULL) {
+ print_error("Failed to open file '%s' while storing database: %s.", path, strerror(errno));
+ return false;
+ }
+
+ fwrite(DB_FILE_SIGN, SIZEOF_CHAR, DB_FILE_SIGN_LENGTH, file);
+ fwrite(db, SIZEOF_DATABASE_ST, 1, file);
+ fwrite(db->tasks, SIZEOF_TASK_ST, db->count, file);
+
+ fclose(file);
+ return true;
}
// Loads data from binary file into database.
// Returns success.
bool load_database(database_st *db, const char *path) {
- assert(db != NULL);
- assert(path != NULL);
-
- // Open file.
- FILE *file = fopen(path, "rb");
- if (file == NULL) {
- print_error("Failed to open file '%s' while loading database: %s.", path, 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) {
- print_error("Invalid file signature.");
- fclose(file);
- return false;
- }
-
- // Read database structure.
- fread(db, SIZEOF_DATABASE_ST, 1, file);
-
- // Reserve database capacity for tasks.
- size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST;
- db->tasks = malloc(capacity_bytes);
- if (db->tasks == NULL && capacity_bytes > 0) {
- print_error("Failed to allocate memory while loading database: %s.", strerror(errno));
- return false;
- }
-
- // Read database tasks.
- fread(db->tasks, SIZEOF_TASK_ST, db->count, file);
-
- // Make sure we are reading all the file.
- assert(fgetc(file) == EOF);
-
- fclose(file);
- return true;
+ assert(db != NULL);
+ assert(path != NULL);
+
+ // Open file.
+ FILE *file = fopen(path, "rb");
+ if (file == NULL) {
+ print_error("Failed to open file '%s' while loading database: %s.", path, 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) {
+ print_error("Invalid file signature.");
+ fclose(file);
+ return false;
+ }
+
+ // Read database structure.
+ fread(db, SIZEOF_DATABASE_ST, 1, file);
+
+ // Reserve database capacity for tasks.
+ size_t capacity_bytes = db->capacity * SIZEOF_TASK_ST;
+ db->tasks = malloc(capacity_bytes);
+ if (db->tasks == NULL && capacity_bytes > 0) {
+ print_error("Failed to allocate memory while loading database: %s.", strerror(errno));
+ return false;
+ }
+
+ // Read database tasks.
+ fread(db->tasks, SIZEOF_TASK_ST, 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_st *db, const char *path) {
- assert(db != NULL);
- assert(path != NULL);
-
- FILE *file = fopen(path, "w");
- if (file == NULL) {
- print_error("Failed to open file '%s' while exporting to CSV: %s.", path, 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[TASK_NAME_BYTES];
- for (size_t idx = 0; idx < db->count; idx++) {
- task_st *task = &db->tasks[idx];
- memcpy(name, task->name, TASK_NAME_BYTES);
- 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;
+ assert(db != NULL);
+ assert(path != NULL);
+
+ FILE *file = fopen(path, "w");
+ if (file == NULL) {
+ print_error("Failed to open file '%s' while exporting to CSV: %s.", path, 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[TASK_NAME_BYTES];
+ for (size_t idx = 0; idx < db->count; idx++) {
+ task_st *task = &db->tasks[idx];
+ memcpy(name, task->name, TASK_NAME_BYTES);
+ 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_st *db, const char *path) {
- assert(db != NULL);
- assert(path != NULL);
-
- FILE *file = fopen(path, "r");
- if (file == NULL) {
- print_error("Failed to open file '%s' while importing from CSV: %s.", path, strerror(errno));
- return false;
- }
-
- // Skip header line.
- fscanf(file, "%*[^\n]\n");
-
- // Parse CSV file.
- char *csv_buffer = NULL;
- size_t csv_buffer_size = 0;
- while(getline(&csv_buffer, &csv_buffer_size, file) != -1) { // Check if reached EOF.
-
- // Find task name string limits.
- char *name_delimiter = strchr(csv_buffer, ',');
- if (name_delimiter == NULL) {
- continue;
- }
- size_t name_length = (name_delimiter - csv_buffer);
- if (name_length > TASK_NAME_LENGTH) {
- name_length = TASK_NAME_LENGTH;
- }
-
- // Prepare new task.
- task_st *task;
- if (create_task(db, &task) == false) {
- return false;
- }
-
- // Import task name.
- memcpy(task->name, csv_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]
- ) != NUM_WEEK_DAYS
- ) {
- replace_char(csv_buffer, '\n', ' ');
- print_error("Discarding invalid line '%s' and continuing.", csv_buffer);
- delete_task(db, task);
- continue;
- }
-
- // Add task timer values to total timers.
- for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) {
- db->total_times[idx] = add_int64(db->total_times[idx], task->times[idx]);
- }
- }
-
- fclose(file);
- free(csv_buffer);
- return true;
+ assert(db != NULL);
+ assert(path != NULL);
+
+ FILE *file = fopen(path, "r");
+ if (file == NULL) {
+ print_error("Failed to open file '%s' while importing from CSV: %s.", path, strerror(errno));
+ return false;
+ }
+
+ // Skip header line.
+ fscanf(file, "%*[^\n]\n");
+
+ // Parse CSV file.
+ char *csv_buffer = NULL;
+ size_t csv_buffer_size = 0;
+ while(getline(&csv_buffer, &csv_buffer_size, file) != -1) { // Check if reached EOF.
+
+ // Find task name string limits.
+ char *name_delimiter = strchr(csv_buffer, ',');
+ if (name_delimiter == NULL) {
+ continue;
+ }
+ size_t name_length = (name_delimiter - csv_buffer);
+ if (name_length > TASK_NAME_LENGTH) {
+ name_length = TASK_NAME_LENGTH;
+ }
+
+ // Prepare new task.
+ task_st *task;
+ if (create_task(db, &task) == false) {
+ return false;
+ }
+
+ // Import task name.
+ memcpy(task->name, csv_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]
+ ) != NUM_WEEK_DAYS
+ ) {
+ replace_char(csv_buffer, '\n', ' ');
+ print_error("Discarding invalid line '%s' and continuing.", csv_buffer);
+ delete_task(db, task);
+ continue;
+ }
+
+ // Add task timer values to total timers.
+ for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) {
+ db->total_times[idx] = add_int64(db->total_times[idx], task->times[idx]);
+ }
+ }
+
+ fclose(file);
+ free(csv_buffer);
+ 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);
-
- FILE *file = fopen(path, "a+");
- if (file == NULL) {
- print_error("Failed to open file '%s' while appending to CSV: %s.", path, strerror(errno));
- return false;
- }
-
- char last_char;
- fseek(file, -1, SEEK_END);
- fread(&last_char, SIZEOF_CHAR, 1, file);
- if (last_char != '\n') {
- fprintf(file, "\n");
- }
-
- char name[TASK_NAME_BYTES];
- memcpy(name, task->name, TASK_NAME_BYTES);
- 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;
+ assert(task != NULL);
+ assert(path != NULL);
+
+ FILE *file = fopen(path, "a+");
+ if (file == NULL) {
+ print_error("Failed to open file '%s' while appending to CSV: %s.", path, strerror(errno));
+ return false;
+ }
+
+ char last_char;
+ fseek(file, -1, SEEK_END);
+ fread(&last_char, SIZEOF_CHAR, 1, file);
+ if (last_char != '\n') {
+ fprintf(file, "\n");
+ }
+
+ char name[TASK_NAME_BYTES];
+ memcpy(name, task->name, TASK_NAME_BYTES);
+ 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;
}
// Selects task by index.
// Index gets clamped to [0, db->count[.
void select_task_by_index(database_st *db, ptrdiff_t index) {
- assert(db != NULL);
- db->selected_task = db->count == 0 ? -1
- : index < 0 ? 0
- : index >= db->count ? db->count - 1
- : index;
+ assert(db != NULL);
+ db->selected_task = db->count == 0 ? -1
+ : index < 0 ? 0
+ : index >= db->count ? db->count - 1
+ : index;
}
// Selects task by delta relative to currently selected task.
void select_task_by_delta(database_st *db, ptrdiff_t delta) {
- assert(db != NULL);
- ptrdiff_t idx = (delta > 0 && db->selected_task > PTRDIFF_MAX - delta) ? PTRDIFF_MAX
- : (delta < 0 && db->selected_task < PTRDIFF_MIN + delta) ? PTRDIFF_MIN
- : db->selected_task + delta;
- select_task_by_index(db, idx);
+ assert(db != NULL);
+ ptrdiff_t idx = (delta > 0 && db->selected_task > PTRDIFF_MAX - delta) ? PTRDIFF_MAX
+ : (delta < 0 && db->selected_task < PTRDIFF_MIN + delta) ? PTRDIFF_MIN
+ : db->selected_task + delta;
+ select_task_by_index(db, idx);
}
// Selects task.
void select_task(database_st *db, task_st *task) {
- assert(db != NULL);
- assert(task != NULL);
- assert(task >= db->tasks && task - db->tasks < db->count);
- db->selected_task = task - db->tasks;
+ assert(db != NULL);
+ assert(task != NULL);
+ assert(task >= db->tasks && task - db->tasks < db->count);
+ db->selected_task = task - db->tasks;
}
// Set active task.
// Passing task as NULL de-activates any previously active task.
void set_active_task(database_st *db, task_st *task) {
- assert(db != NULL);
- assert(task == NULL || (task >= db->tasks && task < &db->tasks[db->count]));
- update_times(db);
- db->active_task = (task == NULL) ? -1 : task - db->tasks;
+ assert(db != NULL);
+ assert(task == NULL || (task >= db->tasks && task < &db->tasks[db->count]));
+ update_times(db);
+ db->active_task = (task == NULL) ? -1 : task - db->tasks;
}
// Returns true when database is full.
bool is_database_full(database_st *db) {
- assert(db != NULL);
- return db->count >= MAX_DATABASE_TASKS;
+ assert(db != NULL);
+ return db->count >= MAX_DATABASE_TASKS;
}
-#define INPUT_TIMEOUT_MS 1000
-#define INPUT_AWAIT_INF -1
+#define INPUT_TIMEOUT_MS 1000
+#define INPUT_AWAIT_INF -1
-#define NUM_HEADER_ROWS 1
-#define NUM_FOOTER_ROWS 1
-#define NUM_COLUMNS 9
+#define NUM_HEADER_ROWS 1
+#define NUM_FOOTER_ROWS 1
+#define NUM_COLUMNS 9
-#define L_TITLE_IDX 0
-#define L_DAYS_IDX 1
-#define L_TOTAL_IDX 8
+#define L_TITLE_IDX 0
+#define L_DAYS_IDX 1
+#define L_TOTAL_IDX 8
typedef enum {
- L_NORMAL,
- L_COMPACT,
- NUM_LAYOUTS,
+ L_NORMAL,
+ L_COMPACT,
+ NUM_LAYOUTS,
} layouts_et;
typedef struct {
- char *header;
- int width;
- int alignment_offset;
- char alignment;
+ char *header;
+ int width;
+ int alignment_offset;
+ char alignment;
} column_st;
typedef struct {
- column_st columns[NUM_COLUMNS];
- char *archive_title;
+ column_st columns[NUM_COLUMNS];
+ char *archive_title;
} layout_st;
-layout_st layouts[NUM_LAYOUTS];
-int layout_tasks_rows;
-bool is_terminal_too_small = true;
+layout_st layouts[NUM_LAYOUTS];
+int layout_tasks_rows;
+bool is_terminal_too_small = true;
void initialize_tui() {
-
- // Normal layout.
- layouts[L_NORMAL] = (layout_st) {
- .archive_title = " Archive ",
- .columns = {
- { .header = " Task Time Tracker v" VERSION " ", .width = -1, .alignment = 'L' },
- { .header = " Sun ", .width = 7, .alignment = 'C' },
- { .header = " Mon ", .width = 7, .alignment = 'C' },
- { .header = " Tue ", .width = 7, .alignment = 'C' },
- { .header = " Wed ", .width = 7, .alignment = 'C' },
- { .header = " Thu ", .width = 7, .alignment = 'C' },
- { .header = " Fri ", .width = 7, .alignment = 'C' },
- { .header = " Sat ", .width = 7, .alignment = 'C' },
- { .header = " Total ", .width = 9, .alignment = 'C' },
- }
- };
-
- // Compact layout.
- layouts[L_COMPACT] = (layout_st) {
- .archive_title = " Archive ",
- .columns = {
- { .header = " TTT " VERSION " ", .width = -1, .alignment = 'L' },
- { .header = " S ", .width = 5, .alignment = 'C' },
- { .header = " M ", .width = 5, .alignment = 'C' },
- { .header = " T ", .width = 5, .alignment = 'C' },
- { .header = " W ", .width = 5, .alignment = 'C' },
- { .header = " T ", .width = 5, .alignment = 'C' },
- { .header = " F ", .width = 5, .alignment = 'C' },
- { .header = " S ", .width = 5, .alignment = 'C' },
- { .header = " # ", .width = 5, .alignment = 'C' },
- }
- };
-
- // Calculate alignment_offsets.
- for (layout_st *layout = layouts; layout < layouts + NUM_LAYOUTS; layout++) {
- for (column_st *col = layout->columns; col < layout->columns + NUM_COLUMNS; col++) {
- int offset;
- switch(col->alignment) {
- default:
- case 'L':
- offset = 0;
- break;
-
- case 'C':
- offset = ((col->width - strlen(col->header)) / 2);
- break;
-
- case 'R':
- offset = (col->width - strlen(col->header));
- break;
- }
- col->alignment_offset = 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.
- noecho(); // Disable echoing input characters.
-
- // Initialize pairs of colors.
- start_color();
- use_default_colors(); // Using default (-1) instead of COLOR_BLACK.
- init_pair(STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN);
- init_pair(STYLE_SELECTED_INVERTED, COLOR_CYAN, -1);
- init_pair(STYLE_ACTIVE, COLOR_BLUE, -1);
- init_pair(STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE);
- init_pair(STYLE_ERROR, COLOR_RED, -1);
+
+ // Normal layout.
+ layouts[L_NORMAL] = (layout_st) {
+ .archive_title = " Archive ",
+ .columns = {
+ { .header = " Task Time Tracker v" VERSION " ", .width = -1, .alignment = 'L' },
+ { .header = " Sun ", .width = 7, .alignment = 'C' },
+ { .header = " Mon ", .width = 7, .alignment = 'C' },
+ { .header = " Tue ", .width = 7, .alignment = 'C' },
+ { .header = " Wed ", .width = 7, .alignment = 'C' },
+ { .header = " Thu ", .width = 7, .alignment = 'C' },
+ { .header = " Fri ", .width = 7, .alignment = 'C' },
+ { .header = " Sat ", .width = 7, .alignment = 'C' },
+ { .header = " Total ", .width = 9, .alignment = 'C' },
+ }
+ };
+
+ // Compact layout.
+ layouts[L_COMPACT] = (layout_st) {
+ .archive_title = " Archive ",
+ .columns = {
+ { .header = " TTT " VERSION " ", .width = -1, .alignment = 'L' },
+ { .header = " S ", .width = 5, .alignment = 'C' },
+ { .header = " M ", .width = 5, .alignment = 'C' },
+ { .header = " T ", .width = 5, .alignment = 'C' },
+ { .header = " W ", .width = 5, .alignment = 'C' },
+ { .header = " T ", .width = 5, .alignment = 'C' },
+ { .header = " F ", .width = 5, .alignment = 'C' },
+ { .header = " S ", .width = 5, .alignment = 'C' },
+ { .header = " # ", .width = 5, .alignment = 'C' },
+ }
+ };
+
+ // Calculate alignment_offsets.
+ for (layout_st *layout = layouts; layout < layouts + NUM_LAYOUTS; layout++) {
+ for (column_st *col = layout->columns; col < layout->columns + NUM_COLUMNS; col++) {
+ int offset;
+ switch(col->alignment) {
+ default:
+ case 'L':
+ offset = 0;
+ break;
+
+ case 'C':
+ offset = ((col->width - strlen(col->header)) / 2);
+ break;
+
+ case 'R':
+ offset = (col->width - strlen(col->header));
+ break;
+ }
+ col->alignment_offset = 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.
+ noecho(); // Disable echoing input characters.
+
+ // Initialize pairs of colors.
+ start_color();
+ use_default_colors(); // Using default (-1) instead of COLOR_BLACK.
+ init_pair(STYLE_SELECTED, COLOR_BLACK, COLOR_CYAN);
+ init_pair(STYLE_SELECTED_INVERTED, COLOR_CYAN, -1);
+ init_pair(STYLE_ACTIVE, COLOR_BLUE, -1);
+ init_pair(STYLE_ACTIVE_SELECTED, COLOR_WHITE, COLOR_BLUE);
+ init_pair(STYLE_ERROR, COLOR_RED, -1);
}
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++) {
- layout->columns[0].width -= layout->columns[idx].width;
- }
- }
+ // 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++) {
+ layout->columns[0].width -= layout->columns[idx].width;
+ }
+ }
}
void draw_tui(database_st *db, layout_st *layout) {
-
- const static int adjust_first_day_of_week[] = {
- (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- (2 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- (3 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- (4 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- (5 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- (6 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
- };
-
- int x, y;
- column_st *col;
-
- // Get context information.
- task_st *active_task = get_active_task(db);
- task_st *selected_task = get_selected_task(db);
- time_t now_utc = time(NULL);
- int now_week_day = localtime(&now_utc)->tm_wday;
-
- // Reset theme and clear screen.
- attrset(A_NORMAL);
- erase();
-
- // Draw outer border.
- box(stdscr, 0, 0);
-
- // Draw table grids.
- y = 0;
- x = 0;
- for (int idx = 0; idx < NUM_COLUMNS - 1; idx++) {
- x += 1 + layout->columns[idx].width;
- mvaddch(y, x, ACS_TTEE);
- for (y = 1; y < size_y - 1; y++) {
- mvaddch(y, x, ACS_VLINE);
- }
- mvaddch(size_y - 1, x, ACS_BTEE);
- }
-
-
- ///////////////////////////////////////////////////////////////////////////
- // Draw headers.
- y = 0;
- x = 0;
-
- // Headers : title
- x++;
- col = &layout->columns[L_TITLE_IDX];
- mvaddstr(y, x + col->alignment_offset, (db == &archive ? layout->archive_title : col->header));
- x += col->width;
-
- // Headers : days
- for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) {
- int idx = adjust_first_day_of_week[raw_idx];
- x++;
-
- // Apply theme.
- if (idx == now_week_day && active_task != NULL) {
- attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD);
- }
- else if (idx == now_week_day) {
- attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD);
- }
-
- col = &layout->columns[L_DAYS_IDX + idx];
- mvaddstr(y, x + col->alignment_offset, col->header);
- x += col->width;
-
- // Reset theme.
- attrset(A_NORMAL);
- }
-
- // Headers : total
- x++;
- col = &layout->columns[L_TOTAL_IDX];
- mvaddstr(y, x + col->alignment_offset, col->header);
-
-
- ///////////////////////////////////////////////////////////////////////////
- // Draw tasks.
-
- uint64_t total_time = 0;
- int column_width;
-
- y = 0;
- // Pagination based on currently selected task (show page where selected task is).
- size_t idx_start = (db->selected_task / layout_tasks_rows) * layout_tasks_rows;
- // Display up to rows allowed by the layout, or less if reached end of database.
- 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++;
- x = 0;
-
- // Apply theme.
- if (task == active_task && task == selected_task) {
- attron(COLOR_PAIR(STYLE_ACTIVE_SELECTED) | A_BOLD);
- }
- else if (task == selected_task) {
- attron(COLOR_PAIR(STYLE_SELECTED));
- }
- else if (task == active_task) {
- attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD);
- }
-
- // Task title.
- x++;
- column_width = layout->columns[L_TITLE_IDX].width;
- mvprintw(y, x, "%-*.*s", column_width, column_width, task->name);
- x += column_width;
-
- // Task times.
- total_time = 0;
- for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) {
- x++;
-
- int day_idx = (idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS;
-
- column_width = layout->columns[L_DAYS_IDX + day_idx].width;
- int64_t task_stime = task->times[day_idx];
- total_time = add_int64(total_time, task_stime);
- mvprintw_time(y, x, task_stime, column_width);
- x += column_width;
- }
-
- // Task total.
- x++;
- mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width);
-
- // Reset theme.
- attrset(A_NORMAL);
- }
-
-
- ///////////////////////////////////////////////////////////////////////////
- // Draw selected/total tasks.
- int size = snprintf(NULL, 0, " %td/%zd ", db->selected_task + 1, db->count);
- if (size <= layout->columns[L_TITLE_IDX].width) {
- mvprintw(size_y - 1, 1, " %td/%zd ", db->selected_task + 1, db->count);
- }
- else {
- mvprintw(size_y - 1, 1, "%td", db->selected_task + 1);
- }
-
- ///////////////////////////////////////////////////////////////////////////
- // Draw daily totals.
- y = size_y - 1;
- x = 0 + 1 + layout->columns[L_TITLE_IDX].width;
- total_time = 0;
- for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) {
- int idx = adjust_first_day_of_week[raw_idx];
- int64_t daily_total = db->total_times[idx];
- x++;
-
- // Apply theme.
- if (idx == now_week_day && active_task != NULL) {
- attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD);
- }
- else if (idx == now_week_day) {
- attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD);
- }
-
- column_width = layout->columns[L_DAYS_IDX + idx].width;
- total_time = add_int64(total_time, daily_total);
- mvprintw_time(y, x, daily_total, column_width);
- x += column_width;
-
- // Reset theme.
- attrset(A_NORMAL);
- }
- x++;
- mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width);
+
+ const static int adjust_first_day_of_week[] = {
+ (0 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ (1 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ (2 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ (3 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ (4 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ (5 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ (6 + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS,
+ };
+
+ int x, y;
+ column_st *col;
+
+ // Get context information.
+ task_st *active_task = get_active_task(db);
+ task_st *selected_task = get_selected_task(db);
+ time_t now_utc = time(NULL);
+ int now_week_day = localtime(&now_utc)->tm_wday;
+
+ // Reset theme and clear screen.
+ attrset(A_NORMAL);
+ erase();
+
+ // Draw outer border.
+ box(stdscr, 0, 0);
+
+ // Draw table grids.
+ y = 0;
+ x = 0;
+ for (int idx = 0; idx < NUM_COLUMNS - 1; idx++) {
+ x += 1 + layout->columns[idx].width;
+ mvaddch(y, x, ACS_TTEE);
+ for (y = 1; y < size_y - 1; y++) {
+ mvaddch(y, x, ACS_VLINE);
+ }
+ mvaddch(size_y - 1, x, ACS_BTEE);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw headers.
+ y = 0;
+ x = 0;
+
+ // Headers : title
+ x++;
+ col = &layout->columns[L_TITLE_IDX];
+ mvaddstr(y, x + col->alignment_offset, (db == &archive ? layout->archive_title : col->header));
+ x += col->width;
+
+ // Headers : days
+ for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) {
+ int idx = adjust_first_day_of_week[raw_idx];
+ x++;
+
+ // Apply theme.
+ if (idx == now_week_day && active_task != NULL) {
+ attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD);
+ }
+ else if (idx == now_week_day) {
+ attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD);
+ }
+
+ col = &layout->columns[L_DAYS_IDX + idx];
+ mvaddstr(y, x + col->alignment_offset, col->header);
+ x += col->width;
+
+ // Reset theme.
+ attrset(A_NORMAL);
+ }
+
+ // Headers : total
+ x++;
+ col = &layout->columns[L_TOTAL_IDX];
+ mvaddstr(y, x + col->alignment_offset, col->header);
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw tasks.
+
+ uint64_t total_time = 0;
+ int column_width;
+
+ y = 0;
+ // Pagination based on currently selected task (show page where selected task is).
+ size_t idx_start = (db->selected_task / layout_tasks_rows) * layout_tasks_rows;
+ // Display up to rows allowed by the layout, or less if reached end of database.
+ 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++;
+ x = 0;
+
+ // Apply theme.
+ if (task == active_task && task == selected_task) {
+ attron(COLOR_PAIR(STYLE_ACTIVE_SELECTED) | A_BOLD);
+ }
+ else if (task == selected_task) {
+ attron(COLOR_PAIR(STYLE_SELECTED));
+ }
+ else if (task == active_task) {
+ attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD);
+ }
+
+ // Task title.
+ x++;
+ column_width = layout->columns[L_TITLE_IDX].width;
+ mvprintw(y, x, "%-*.*s", column_width, column_width, task->name);
+ x += column_width;
+
+ // Task times.
+ total_time = 0;
+ for (int idx = 0; idx < NUM_WEEK_DAYS; idx++) {
+ x++;
+
+ int day_idx = (idx + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS;
+
+ column_width = layout->columns[L_DAYS_IDX + day_idx].width;
+ int64_t task_stime = task->times[day_idx];
+ total_time = add_int64(total_time, task_stime);
+ mvprintw_time(y, x, task_stime, column_width);
+ x += column_width;
+ }
+
+ // Task total.
+ x++;
+ mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width);
+
+ // Reset theme.
+ attrset(A_NORMAL);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw selected/total tasks.
+ int size = snprintf(NULL, 0, " %td/%zd ", db->selected_task + 1, db->count);
+ if (size <= layout->columns[L_TITLE_IDX].width) {
+ mvprintw(size_y - 1, 1, " %td/%zd ", db->selected_task + 1, db->count);
+ }
+ else {
+ mvprintw(size_y - 1, 1, "%td", db->selected_task + 1);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Draw daily totals.
+ y = size_y - 1;
+ x = 0 + 1 + layout->columns[L_TITLE_IDX].width;
+ total_time = 0;
+ for (int raw_idx = 0; raw_idx < NUM_WEEK_DAYS; raw_idx++) {
+ int idx = adjust_first_day_of_week[raw_idx];
+ int64_t daily_total = db->total_times[idx];
+ x++;
+
+ // Apply theme.
+ if (idx == now_week_day && active_task != NULL) {
+ attron(COLOR_PAIR(STYLE_ACTIVE) | A_BOLD);
+ }
+ else if (idx == now_week_day) {
+ attron(COLOR_PAIR(STYLE_SELECTED_INVERTED) | A_BOLD);
+ }
+
+ column_width = layout->columns[L_DAYS_IDX + idx].width;
+ total_time = add_int64(total_time, daily_total);
+ mvprintw_time(y, x, daily_total, column_width);
+ x += column_width;
+
+ // Reset theme.
+ attrset(A_NORMAL);
+ }
+ x++;
+ mvprintw_time(y, x, total_time, layout->columns[L_TOTAL_IDX].width);
}
void *mem_alloc(size_t mem_size, const char *error_tag) {
- void *mem_pointer = malloc(mem_size);
- if (mem_pointer == NULL && mem_size > 0) {
- print_error("Failed to allocate memory (%s): %s.", (error_tag == NULL ? "undefined" : error_tag), strerror(errno));
- exit(EXIT_FAILURE);
- }
- return mem_pointer;
+ void *mem_pointer = malloc(mem_size);
+ if (mem_pointer == NULL && mem_size > 0) {
+ print_error("Failed to allocate memory (%s): %s.", (error_tag == NULL ? "undefined" : error_tag), strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ return mem_pointer;
}
void free_memory() {
- reset_database(&database);
- reset_database(&archive);
-
- free(string_buffer); string_buffer = NULL;
- free(app_folder); app_folder = NULL;
- free(db_file_path); db_file_path = NULL;
- free(ar_file_path); ar_file_path = NULL;
+ reset_database(&database);
+ reset_database(&archive);
+
+ free(string_buffer); string_buffer = NULL;
+ free(app_folder); app_folder = NULL;
+ free(db_file_path); db_file_path = NULL;
+ free(ar_file_path); ar_file_path = NULL;
}
bool initialize_app_folder() {
- size_t temp_size;
-
- char *home_path = getenv(HOME_PATH_ENV);
- if (home_path == NULL) {
- home_path = ".";
- }
- temp_size = strlen(home_path) + 1 + strlen(APP_FOLDER_NAME) + 1; // Add space for folder separator and '\0'.
- app_folder = mem_alloc(temp_size, "app folder");
- snprintf(app_folder, temp_size, "%s/%s", home_path, APP_FOLDER_NAME);
-
- // Create app folder.
- mkdir(app_folder, 0740);
- if (errno != 0 && errno != EEXIST) {
- print_error("Failed to create app folder '%s': %s.", app_folder, strerror(errno));
- return false;
- }
-
- // Set database file path.
- temp_size = strlen(app_folder) + 1 + strlen(DB_FILE_NAME) + 1; // Add space for folder separator and '\0'.
- db_file_path = mem_alloc(temp_size, "database file path");
- snprintf(db_file_path, temp_size, "%s/%s", app_folder, DB_FILE_NAME);
-
- // Set archive file path.
- temp_size = strlen(app_folder) + 1 + strlen(AR_FILE_NAME) + 1; // Add space for folder separator and '\0'.
- ar_file_path = mem_alloc(temp_size, "archive file path");
- snprintf(ar_file_path, temp_size, "%s/%s", app_folder, AR_FILE_NAME);
-
- return true;
+ size_t temp_size;
+
+ char *home_path = getenv(HOME_PATH_ENV);
+ if (home_path == NULL) {
+ home_path = ".";
+ }
+ temp_size = strlen(home_path) + 1 + strlen(APP_FOLDER_NAME) + 1; // Add space for folder separator and '\0'.
+ app_folder = mem_alloc(temp_size, "app folder");
+ snprintf(app_folder, temp_size, "%s/%s", home_path, APP_FOLDER_NAME);
+
+ // Create app folder.
+ mkdir(app_folder, 0740);
+ if (errno != 0 && errno != EEXIST) {
+ print_error("Failed to create app folder '%s': %s.", app_folder, strerror(errno));
+ return false;
+ }
+
+ // Set database file path.
+ temp_size = strlen(app_folder) + 1 + strlen(DB_FILE_NAME) + 1; // Add space for folder separator and '\0'.
+ db_file_path = mem_alloc(temp_size, "database file path");
+ snprintf(db_file_path, temp_size, "%s/%s", app_folder, DB_FILE_NAME);
+
+ // Set archive file path.
+ temp_size = strlen(app_folder) + 1 + strlen(AR_FILE_NAME) + 1; // Add space for folder separator and '\0'.
+ ar_file_path = mem_alloc(temp_size, "archive file path");
+ snprintf(ar_file_path, temp_size, "%s/%s", app_folder, AR_FILE_NAME);
+
+ return true;
}
void exit_gracefully(int signal) {
- flushinp();
- ungetch('q');
+ flushinp();
+ ungetch('q');
}
void read_input_to_string_buffer_with_space(int row, int column, int style, int length, int space) {
- assert(length < string_buffer_size);
- assert(space < string_buffer_size);
-
- attron(style | A_UNDERLINE);
- mvprintw(row, column, "%*s", space, "");
- echo();
- curs_set(1);
- memset(string_buffer, 0, string_buffer_size);
- mvgetnstr(row, column, string_buffer, length);
- truncate_string_utf8(string_buffer, length);
- noecho();
- curs_set(0);
- attrset(A_NORMAL);
+ assert(length < string_buffer_size);
+ assert(space < string_buffer_size);
+
+ attron(style | A_UNDERLINE);
+ mvprintw(row, column, "%*s", space, "");
+ echo();
+ curs_set(1);
+ memset(string_buffer, 0, string_buffer_size);
+ mvgetnstr(row, column, string_buffer, length);
+ truncate_string_utf8(string_buffer, length);
+ noecho();
+ curs_set(0);
+ attrset(A_NORMAL);
}
void read_input_to_string_buffer(int row, int column, int style, int length) {
- read_input_to_string_buffer_with_space(row, column, style, length, length);
+ read_input_to_string_buffer_with_space(row, column, style, length, length);
}
// Returns success.
bool read_input_to_int(int row, int style, const char *message, intmax_t *result) {
- assert(message != NULL);
- assert(result != NULL);
-
- attron(style);
- move(row, 1);
- addch(ACS_CKBOARD);
- addstr(message);
- attrset(A_NORMAL);
-
- // Get line number.
- int input_pos_x = getcurx(stdscr);
- int input_width = size_x - input_pos_x - 1;
- read_input_to_string_buffer(row, input_pos_x, style, input_width);
-
- char *parser;
- errno = 0;
- *result = strtoimax(string_buffer, &parser, 10);
-
- bool success = (errno == 0 || errno == ERANGE) // No error OR value was clamped to limits (acceptable).
- && parser != string_buffer; // If no digits are found, parser will return the address of the input string.
-
- return success;
+ assert(message != NULL);
+ assert(result != NULL);
+
+ attron(style);
+ move(row, 1);
+ addch(ACS_CKBOARD);
+ addstr(message);
+ attrset(A_NORMAL);
+
+ // Get line number.
+ int input_pos_x = getcurx(stdscr);
+ int input_width = size_x - input_pos_x - 1;
+ read_input_to_string_buffer(row, input_pos_x, style, input_width);
+
+ char *parser;
+ errno = 0;
+ *result = strtoimax(string_buffer, &parser, 10);
+
+ bool success = (errno == 0 || errno == ERANGE) // No error OR value was clamped to limits (acceptable).
+ && parser != string_buffer; // If no digits are found, parser will return the address of the input string.
+
+ return success;
}
// Retuns true if user presses enter, false otherwise.
bool read_enter_confirmation(int row, int style, const char *message) {
- assert(message != NULL);
-
- attron(style);
- move(row, 1);
- for (int idx = 0; idx < size_x - 2; idx++) {
- addch(ACS_CKBOARD);
- }
- mvaddstr(row, 2, message);
- attrset(A_NORMAL);
-
- return getch() == '\n';
+ assert(message != NULL);
+
+ attron(style);
+ move(row, 1);
+ for (int idx = 0; idx < size_x - 2; idx++) {
+ addch(ACS_CKBOARD);
+ }
+ mvaddstr(row, 2, message);
+ attrset(A_NORMAL);
+
+ return getch() == '\n';
}
int main(int argc, char *argv[]) {
-
- if (initialize_app_folder() == false) {
- print_error("Failed to initialize app folder.");
- free_memory();
- return EXIT_FAILURE;
- }
-
- db = &database;
- reset_database(&database);
- reset_database(&archive);
-
- if (is_file_accessible(db_file_path) == false) {
- if (store_database(&database, db_file_path) == false) {
- print_error("Failed to initialize database.");
- free_memory();
- return EXIT_FAILURE;
- }
- }
-
- if (is_file_accessible(ar_file_path) == false) {
- if (export_to_csv(&archive, ar_file_path) == false) {
- print_error("Failed to initialize archive.");
- free_memory();
- return EXIT_FAILURE;
- }
- }
-
- if (argc > 1) {
- bool is_exit_requested = false;
- for (unsigned idx = 1; idx < argc; idx++) {
- if (is_equal_to_any(argv[idx], "--help", "-h")) {
- fprintf(stdout,
- "Usage: ttt [OPTION]... [FILE]...\n"
- " -i, --import-csv [FILE] Import CSV file to database (discard first row).\n"
- " -e, --export-csv [FILE] Export database to CSV file.\n"
- " -n, --no-autosave Disable autosave feature (only save on exit).\n"
- " -h, --help Display this help and exit.\n"
- " -v, --version Output version information and exit.\n"
- "\n"
- "In app commands\n"
- " a, A Archive selected task (except if active).\n"
- " u, U Unarchive selected task.\n"
- " t, T Select currently active task (if any).\n"
- " d, D Duplicate selected task.\n"
- " n, N Create new task.\n"
- " m, M Move selected task to position.\n"
- " g, G Select task by position.\n"
- " q, Q Save changes and exit.\n"
- " F2 Rename selected task.\n"
- " F5 Recalculate total times.\n"
- " TAB Toggle archive view.\n"
- " BACKSPACE Reset times for selected task.\n"
- " DELETE Delete selected task (except if active).\n"
- " SPACE, ENTER Toggle selected task as active/inactive.\n"
- " 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n"
- " =# sets # seconds;\n"
- " -# subtracts # seconds;\n"
- " # adds # seconds;\n"
- " #m specifies # as minutes;\n"
- " #h specifies # as hours;\n"
- " #d specifies # as days;\n"
- " #y specifies # as years.\n"
- " UP Select task above.\n"
- " DOWN Select task below.\n"
- " PAGE-UP Select task 1 page above.\n"
- " PAGE-DOWN Select task 1 page below.\n"
- " HOME Select first/top task.\n"
- " END Select last/bottom task.\n"
- "\n"
- "Notes\n"
- "- All data files are stored in '$" HOME_PATH_ENV "/.task_time_tracker'.\n"
- " If $" HOME_PATH_ENV " is undefined, './.task_time_tracker' will be used.\n"
- " The database entries are stored in binary format on the 'database.bin' file.\n"
- " The archived entries are stored in CSV format on the 'archive.csv' file.\n"
- "- During intensive tasks such as saving to file or recalculating totals times,\n"
- " a diamond symbol is shown on the top left corner.\n"
- );
- free_memory();
- return EXIT_SUCCESS;
- }
-
- if (is_equal_to_any(argv[idx], "--version", "-v")) {
- fprintf(stdout, "Task Time Tracker version " VERSION "\n");
- free_memory();
- return EXIT_SUCCESS;
- }
-
- if (is_equal_to_any(argv[idx], "--import-csv", "-i")) {
- idx++;
- if (idx >= argc) {
- print_error("Missing CSV file path to import.");
- free_memory();
- return EXIT_FAILURE;
- }
- if (load_database(&database, db_file_path) == false) {
- print_error("Failed to load database.");
- free_memory();
- return EXIT_FAILURE;
- }
- if (import_from_csv(&database, argv[idx]) == false) {
- print_error("Failed to import CSV file.");
- free_memory();
- return EXIT_FAILURE;
- }
- if (store_database(&database, db_file_path) == false) {
- print_error("Failed to store database.");
- free_memory();
- return EXIT_FAILURE;
- }
- reset_database(&database);
- is_exit_requested = true;
- continue;
- }
-
- if (is_equal_to_any(argv[idx], "--export-csv", "-e")) {
- idx++;
- if (idx >= argc) {
- print_error("Missing CSV file path to export.");
- free_memory();
- return EXIT_FAILURE;
- }
- if (load_database(&database, db_file_path) == false) {
- print_error("Failed to load database.");
- free_memory();
- return EXIT_FAILURE;
- }
- if (export_to_csv(&database, argv[idx]) == false) {
- print_error("Failed to export CSV file.");
- free_memory();
- return EXIT_FAILURE;
- }
- reset_database(&database);
- is_exit_requested = true;
- continue;
- }
-
- if (is_equal_to_any(argv[idx], "--no-autosave", "-n")) {
- is_autosave_enabled = false;
- continue;
- }
-
- print_error("%s: invalid option '%s'.\nTry '%s --help' for more information.", argv[0], argv[idx], argv[0]);
- free_memory();
- return EXIT_FAILURE;
- }
-
- if (is_exit_requested) {
- free_memory();
- return EXIT_SUCCESS;
- }
- }
-
- if (load_database(&database, db_file_path) == false) {
- print_error("Failed to load database.");
- free_memory();
- return EXIT_FAILURE;
- }
-
- initialize_tui();
-
- signal(SIGTERM, exit_gracefully);
- signal(SIGINT, exit_gracefully);
- signal(SIGQUIT, exit_gracefully);
- signal(SIGHUP, exit_gracefully);
-
- flushinp();
- ungetch(KEY_RESIZE);
- for (int key; ((key = getch()) != 'q') && (key != 'Q'); ) {
-
- static layout_st *layout = &layouts[L_COMPACT];
- task_st *active_task = get_active_task(db);
- task_st *selected_task = get_selected_task(db);
- int action_style = A_BOLD | COLOR_PAIR(selected_task == active_task && selected_task != NULL ? STYLE_ACTIVE : STYLE_SELECTED_INVERTED);
- int error_style = A_BOLD | COLOR_PAIR(STYLE_ERROR);
- int selected_task_row = is_terminal_too_small ? 0
- : (db->selected_task < 0) ? 1
- : (db->selected_task % layout_tasks_rows) + NUM_HEADER_ROWS;
-
- timeout(INPUT_AWAIT_INF);
- update_times(&database);
-
- switch(key) {
-
- // When getch() times out.
- case ERR: {
- if (is_autosave_enabled && countdown_to_autosave > 0) {
- countdown_to_autosave -= INPUT_TIMEOUT_MS;
- if (countdown_to_autosave <= 0) {
- show_processing();
- if (db == &archive) {
- export_to_csv(&archive, ar_file_path);
- }
- store_database(&database, db_file_path);
- }
- }
- break;
- }
-
- // When terminal is resized.
- case KEY_RESIZE: {
- clear();
- getmaxyx(stdscr, size_y, size_x);
- is_terminal_too_small = size_x < 60 || size_y < 3;
- size_t new_size = 2047 | TASK_NAME_BYTES | (size_x + 1);
- if (string_buffer_size < new_size) {
- string_buffer_size = new_size;
- string_buffer = realloc(string_buffer, string_buffer_size);
- if (string_buffer == NULL && string_buffer_size > 0) {
- print_error("Failed to allocate memory for string buffer: %s.", strerror(errno));
- flushinp();
- ungetch('q');
- break;
- }
- }
- update_layout();
- layout = &layouts[size_x > 100 ? L_NORMAL : L_COMPACT];
- break;
- }
-
- case 'n':
- case 'N':{
- if (is_database_full(db)) {
- read_enter_confirmation(selected_task_row, error_style, " Unable to create entry: database is full. ");
- break;
- }
-
- // Create new task.
- task_st *new_task;
- if (create_task(db, &new_task) == false) {
- break;
- }
-
- // Set new task name.
- time_t now_utc = time(NULL);
- struct tm *now_local = localtime(&now_utc);
- strftime(new_task->name, TASK_NAME_BYTES, "%Y-%m-%d %H:%M:%S", now_local);
-
- // Select new task.
- select_task(db, new_task);
- selected_task = get_selected_task(db);
- trigger_autosave();
-
- // Force rename action.
- flushinp();
- ungetch(KEY_F(2));
- break;
- }
-
- case KEY_F(2): {
- if (selected_task == NULL) {
- break;
- }
-
- read_input_to_string_buffer_with_space(selected_task_row, 1, action_style, TASK_NAME_LENGTH, size_x - 2);
- if (is_empty_string(string_buffer) == false) {
- replace_char(string_buffer, '\t', ' ');
- replace_char(string_buffer, '\v', ' ');
- replace_char(string_buffer, '\f', ' ');
- replace_char(string_buffer, '\r', ' ');
- memcpy(selected_task->name, string_buffer, TASK_NAME_BYTES);
- trigger_autosave();
- }
- break;
- }
-
- case KEY_BACKSPACE: {
- if (selected_task == NULL) {
- break;
- }
-
- if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) {
- reset_task_times(db, selected_task);
- trigger_autosave();
- }
- break;
- }
-
- case KEY_DC: { // Delete
- if (selected_task == NULL || selected_task == active_task) {
- break;
- }
-
- if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) {
- delete_task(db, selected_task);
- trigger_autosave();
- }
- break;
- }
-
- case '1':
- case '2':
- case '3':
- case '4':
- case '5':
- case '6':
- case '7': {
- if (selected_task == NULL) {
- break;
- }
-
- // Prepare position to input time operation.
- int selected_day = key - '1';
- 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++;
-
- // Get input string.
- read_input_to_string_buffer(selected_task_row, input_pos_x, action_style, input_width);
- char *input = string_buffer;
-
- // Abort if input if empty.
- if (is_empty_string(input) == true) {
- break;
- }
-
- // Search for assign '=' operator and discard everything before (if found).
- char *assign_str = strchr(input, '=');
- bool is_assign = assign_str != NULL;
- if (is_assign == true) {
- input = assign_str + 1;
- }
-
- // Try to parse a number and abort if it fails.
- char *parser;
- long double input_float = strtold(input, &parser);
- if (parser == input) {
- break;
- }
- input = parser;
-
- // Try to parse a character representing the time multiplier.
- long double multiplier = 1.0;
- for (int i=0; i < strlen(input); i++) {
- char ch = input[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;
- }
- }
-
- // Process input and check if it's valid.
- long double input_time = input_float * multiplier;
- bool is_result_valid = (input_time >= (long double)INT64_MIN && input_time <= (long double)INT64_MAX);
- if (is_result_valid == false) {
- break;
- }
-
- // Apply changes.
- int64_t time = input_time;
- int day = (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS;
- if (is_assign == true) {
- set_task_time(db, selected_task, day, time);
- }
- else {
- add_task_time(db, selected_task, day, time);
- }
- trigger_autosave();
- break;
- }
-
- case 'm':
- case 'M': {
- if (selected_task == NULL) {
- break;
- }
-
- intmax_t value;
- if (read_input_to_int(selected_task_row, action_style, " Move to: ", &value) == false) {
- break;
- }
-
- ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1;
- move_task_to_index(db, selected_task, target_index);
- trigger_autosave();
- break;
- }
-
- case 'g':
- case 'G': {
- if (selected_task == NULL) {
- break;
- }
-
- intmax_t value;
- if (read_input_to_int(selected_task_row, action_style, " Go to: ", &value) == false) {
- break;
- }
-
- ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1;
- select_task_by_index(db, target_index);
- break;
- }
-
- case 'd':
- case 'D':{
- if (selected_task == NULL) {
- break;
- }
-
- if (is_database_full(db)) {
- read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate entry: database is full. ");
- break;
- }
-
- if (duplicate_task(db, selected_task) == false) {
- break;
- }
- trigger_autosave();
- break;
- }
-
- case KEY_F(5): {
- update_total_times(db);
- trigger_autosave();
- break;
- }
-
- case 't':
- case 'T': {
- if (active_task == NULL) {
- break;
- }
- select_task(db, active_task);
- break;
- }
-
- case '\n':
- case ' ': {
- if (db != &database || selected_task == NULL) {
- break;
- }
- set_active_task(db, (active_task == selected_task) ? NULL : selected_task);
- active_task = get_active_task(db);
- trigger_autosave();
- break;
- }
-
- case '\t': {
- if (db == &database) {
- if (import_from_csv(&archive, ar_file_path) == false) {
- reset_database(&archive);
- print_error("Failed to load archive.");
- break;
- }
- db = &archive;
- }
- else {
- if (export_to_csv(&archive, ar_file_path) == false) {
- print_error("Failed to store archive.");
- break;
- }
- reset_database(&archive);
- db = &database;
- }
- break;
- }
-
- case 'a':
- case 'A': {
- if (db != &database || selected_task == NULL || selected_task == active_task) {
- break;
- }
- if (append_to_csv(selected_task, ar_file_path) == false) {
- print_error("Failed to archive entry.");
- break;
- }
- delete_task(&database, selected_task);
- trigger_autosave();
- break;
- }
-
- case 'u':
- case 'U': {
- if (db != &archive || selected_task == NULL) {
- break;
- }
- if (is_database_full(&database)) {
- read_enter_confirmation(selected_task_row, error_style, " Unable to unarchive entry: database is full. ");
- break;
- }
- if (duplicate_task(&database, selected_task) == false) {
- print_error("Failed to unarchive entry.");
- break;
- }
- delete_task(&archive, selected_task);
- trigger_autosave();
- break;
- }
-
- case KEY_HOME: {
- select_task_by_index(db, 0);
- break;
- }
-
- case KEY_UP: {
- select_task_by_delta(db, -1);
- break;
- }
-
- case KEY_PPAGE: {
- select_task_by_delta(db, -layout_tasks_rows);
- break;
- }
-
- case KEY_END: {
- select_task_by_index(db, db->count-1);
- break;
- }
-
- case KEY_DOWN: {
- select_task_by_delta(db, 1);
- break;
- }
-
- case KEY_NPAGE: {
- select_task_by_delta(db, layout_tasks_rows);
- break;
- }
- }
-
- if (is_terminal_too_small) {
- const char *INVALID_WINDOW_MESSAGE = "Terminal is too small: minimum 60x3.";
- const int INVALID_WINDOW_MESSAGE_LENGTH = strlen(INVALID_WINDOW_MESSAGE);
- mvaddstr(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE_LENGTH) / 2, INVALID_WINDOW_MESSAGE);
- }
- else {
- draw_tui(db, layout);
- draw_error_window();
- }
-
- timeout(INPUT_TIMEOUT_MS);
- }
-
- // Save any unsaved changes.
- show_processing();
- bool error_saving = false;
- if (db == &archive) {
- if (export_to_csv(&archive, ar_file_path) == false) {
- print_error("Failed to save archive.");
- error_saving |= true;
- }
- }
- if (countdown_to_autosave > 0 || is_autosave_enabled == false) {
- if (store_database(&database, db_file_path) == false) {
- print_error("Failed to save database.");
- error_saving |= true;
- }
- }
- if (error_saving) {
- print_error("Press any key to close.");
- draw_error_window();
- timeout(INPUT_AWAIT_INF);
- getch();
- }
-
- endwin();
- free_memory();
- return error_saving ? EXIT_FAILURE : EXIT_SUCCESS;
+
+ if (initialize_app_folder() == false) {
+ print_error("Failed to initialize app folder.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+
+ db = &database;
+ reset_database(&database);
+ reset_database(&archive);
+
+ if (is_file_accessible(db_file_path) == false) {
+ if (store_database(&database, db_file_path) == false) {
+ print_error("Failed to initialize database.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (is_file_accessible(ar_file_path) == false) {
+ if (export_to_csv(&archive, ar_file_path) == false) {
+ print_error("Failed to initialize archive.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (argc > 1) {
+ bool is_exit_requested = false;
+ for (unsigned idx = 1; idx < argc; idx++) {
+ if (is_equal_to_any(argv[idx], "--help", "-h")) {
+ fprintf(stdout,
+ "Usage: ttt [OPTION]... [FILE]...\n"
+ " -i, --import-csv [FILE] Import CSV file to database (discard first row).\n"
+ " -e, --export-csv [FILE] Export database to CSV file.\n"
+ " -n, --no-autosave Disable autosave feature (only save on exit).\n"
+ " -h, --help Display this help and exit.\n"
+ " -v, --version Output version information and exit.\n"
+ "\n"
+ "In app commands\n"
+ " a, A Archive selected task (except if active).\n"
+ " u, U Unarchive selected task.\n"
+ " t, T Select currently active task (if any).\n"
+ " d, D Duplicate selected task.\n"
+ " n, N Create new task.\n"
+ " m, M Move selected task to position.\n"
+ " g, G Select task by position.\n"
+ " q, Q Save changes and exit.\n"
+ " F2 Rename selected task.\n"
+ " F5 Recalculate total times.\n"
+ " TAB Toggle archive view.\n"
+ " BACKSPACE Reset times for selected task.\n"
+ " DELETE Delete selected task (except if active).\n"
+ " SPACE, ENTER Toggle selected task as active/inactive.\n"
+ " 1, 2, 3, 4, 5, 6, 7 Edit selected task time for the Nth day of week:\n"
+ " =# sets # seconds;\n"
+ " -# subtracts # seconds;\n"
+ " # adds # seconds;\n"
+ " #m specifies # as minutes;\n"
+ " #h specifies # as hours;\n"
+ " #d specifies # as days;\n"
+ " #y specifies # as years.\n"
+ " UP Select task above.\n"
+ " DOWN Select task below.\n"
+ " PAGE-UP Select task 1 page above.\n"
+ " PAGE-DOWN Select task 1 page below.\n"
+ " HOME Select first/top task.\n"
+ " END Select last/bottom task.\n"
+ "\n"
+ "Notes\n"
+ "- All data files are stored in '$" HOME_PATH_ENV "/.task_time_tracker'.\n"
+ " If $" HOME_PATH_ENV " is undefined, './.task_time_tracker' will be used.\n"
+ " The database entries are stored in binary format on the 'database.bin' file.\n"
+ " The archived entries are stored in CSV format on the 'archive.csv' file.\n"
+ "- During intensive tasks such as saving to file or recalculating totals times,\n"
+ " a diamond symbol is shown on the top left corner.\n"
+ );
+ free_memory();
+ return EXIT_SUCCESS;
+ }
+
+ if (is_equal_to_any(argv[idx], "--version", "-v")) {
+ fprintf(stdout, "Task Time Tracker version " VERSION "\n");
+ free_memory();
+ return EXIT_SUCCESS;
+ }
+
+ if (is_equal_to_any(argv[idx], "--import-csv", "-i")) {
+ idx++;
+ if (idx >= argc) {
+ print_error("Missing CSV file path to import.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ if (load_database(&database, db_file_path) == false) {
+ print_error("Failed to load database.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ if (import_from_csv(&database, argv[idx]) == false) {
+ print_error("Failed to import CSV file.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ if (store_database(&database, db_file_path) == false) {
+ print_error("Failed to store database.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ reset_database(&database);
+ is_exit_requested = true;
+ continue;
+ }
+
+ if (is_equal_to_any(argv[idx], "--export-csv", "-e")) {
+ idx++;
+ if (idx >= argc) {
+ print_error("Missing CSV file path to export.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ if (load_database(&database, db_file_path) == false) {
+ print_error("Failed to load database.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ if (export_to_csv(&database, argv[idx]) == false) {
+ print_error("Failed to export CSV file.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+ reset_database(&database);
+ is_exit_requested = true;
+ continue;
+ }
+
+ if (is_equal_to_any(argv[idx], "--no-autosave", "-n")) {
+ is_autosave_enabled = false;
+ continue;
+ }
+
+ print_error("%s: invalid option '%s'.\nTry '%s --help' for more information.", argv[0], argv[idx], argv[0]);
+ free_memory();
+ return EXIT_FAILURE;
+ }
+
+ if (is_exit_requested) {
+ free_memory();
+ return EXIT_SUCCESS;
+ }
+ }
+
+ if (load_database(&database, db_file_path) == false) {
+ print_error("Failed to load database.");
+ free_memory();
+ return EXIT_FAILURE;
+ }
+
+ initialize_tui();
+
+ signal(SIGTERM, exit_gracefully);
+ signal(SIGINT, exit_gracefully);
+ signal(SIGQUIT, exit_gracefully);
+ signal(SIGHUP, exit_gracefully);
+
+ flushinp();
+ ungetch(KEY_RESIZE);
+ for (int key; ((key = getch()) != 'q') && (key != 'Q'); ) {
+
+ static layout_st *layout = &layouts[L_COMPACT];
+ task_st *active_task = get_active_task(db);
+ task_st *selected_task = get_selected_task(db);
+ int action_style = A_BOLD | COLOR_PAIR(selected_task == active_task && selected_task != NULL ? STYLE_ACTIVE : STYLE_SELECTED_INVERTED);
+ int error_style = A_BOLD | COLOR_PAIR(STYLE_ERROR);
+ int selected_task_row = is_terminal_too_small ? 0
+ : (db->selected_task < 0) ? 1
+ : (db->selected_task % layout_tasks_rows) + NUM_HEADER_ROWS;
+
+ timeout(INPUT_AWAIT_INF);
+ update_times(&database);
+
+ switch(key) {
+
+ // When getch() times out.
+ case ERR: {
+ if (is_autosave_enabled && countdown_to_autosave > 0) {
+ countdown_to_autosave -= INPUT_TIMEOUT_MS;
+ if (countdown_to_autosave <= 0) {
+ show_processing();
+ if (db == &archive) {
+ export_to_csv(&archive, ar_file_path);
+ }
+ store_database(&database, db_file_path);
+ }
+ }
+ break;
+ }
+
+ // When terminal is resized.
+ case KEY_RESIZE: {
+ clear();
+ getmaxyx(stdscr, size_y, size_x);
+ is_terminal_too_small = size_x < 60 || size_y < 3;
+ size_t new_size = 2047 | TASK_NAME_BYTES | (size_x + 1);
+ if (string_buffer_size < new_size) {
+ string_buffer_size = new_size;
+ string_buffer = realloc(string_buffer, string_buffer_size);
+ if (string_buffer == NULL && string_buffer_size > 0) {
+ print_error("Failed to allocate memory for string buffer: %s.", strerror(errno));
+ flushinp();
+ ungetch('q');
+ break;
+ }
+ }
+ update_layout();
+ layout = &layouts[size_x > 100 ? L_NORMAL : L_COMPACT];
+ break;
+ }
+
+ case 'n':
+ case 'N':{
+ if (is_database_full(db)) {
+ read_enter_confirmation(selected_task_row, error_style, " Unable to create entry: database is full. ");
+ break;
+ }
+
+ // Create new task.
+ task_st *new_task;
+ if (create_task(db, &new_task) == false) {
+ break;
+ }
+
+ // Set new task name.
+ time_t now_utc = time(NULL);
+ struct tm *now_local = localtime(&now_utc);
+ strftime(new_task->name, TASK_NAME_BYTES, "%Y-%m-%d %H:%M:%S", now_local);
+
+ // Select new task.
+ select_task(db, new_task);
+ selected_task = get_selected_task(db);
+ trigger_autosave();
+
+ // Force rename action.
+ flushinp();
+ ungetch(KEY_F(2));
+ break;
+ }
+
+ case KEY_F(2): {
+ if (selected_task == NULL) {
+ break;
+ }
+
+ read_input_to_string_buffer_with_space(selected_task_row, 1, action_style, TASK_NAME_LENGTH, size_x - 2);
+ if (is_empty_string(string_buffer) == false) {
+ replace_char(string_buffer, '\t', ' ');
+ replace_char(string_buffer, '\v', ' ');
+ replace_char(string_buffer, '\f', ' ');
+ replace_char(string_buffer, '\r', ' ');
+ memcpy(selected_task->name, string_buffer, TASK_NAME_BYTES);
+ trigger_autosave();
+ }
+ break;
+ }
+
+ case KEY_BACKSPACE: {
+ if (selected_task == NULL) {
+ break;
+ }
+
+ if (read_enter_confirmation(selected_task_row, action_style, " Press enter to reset task. ") == true) {
+ reset_task_times(db, selected_task);
+ trigger_autosave();
+ }
+ break;
+ }
+
+ case KEY_DC: { // Delete
+ if (selected_task == NULL || selected_task == active_task) {
+ break;
+ }
+
+ if (read_enter_confirmation(selected_task_row, action_style, " Press enter to delete task. ") == true) {
+ delete_task(db, selected_task);
+ trigger_autosave();
+ }
+ break;
+ }
+
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7': {
+ if (selected_task == NULL) {
+ break;
+ }
+
+ // Prepare position to input time operation.
+ int selected_day = key - '1';
+ 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++;
+
+ // Get input string.
+ read_input_to_string_buffer(selected_task_row, input_pos_x, action_style, input_width);
+ char *input = string_buffer;
+
+ // Abort if input if empty.
+ if (is_empty_string(input) == true) {
+ break;
+ }
+
+ // Search for assign '=' operator and discard everything before (if found).
+ char *assign_str = strchr(input, '=');
+ bool is_assign = assign_str != NULL;
+ if (is_assign == true) {
+ input = assign_str + 1;
+ }
+
+ // Try to parse a number and abort if it fails.
+ char *parser;
+ long double input_float = strtold(input, &parser);
+ if (parser == input) {
+ break;
+ }
+ input = parser;
+
+ // Try to parse a character representing the time multiplier.
+ long double multiplier = 1.0;
+ for (int i=0; i < strlen(input); i++) {
+ char ch = input[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;
+ }
+ }
+
+ // Process input and check if it's valid.
+ long double input_time = input_float * multiplier;
+ bool is_result_valid = (input_time >= (long double)INT64_MIN && input_time <= (long double)INT64_MAX);
+ if (is_result_valid == false) {
+ break;
+ }
+
+ // Apply changes.
+ int64_t time = input_time;
+ int day = (selected_day + FIRST_DAY_OF_WEEK) % NUM_WEEK_DAYS;
+ if (is_assign == true) {
+ set_task_time(db, selected_task, day, time);
+ }
+ else {
+ add_task_time(db, selected_task, day, time);
+ }
+ trigger_autosave();
+ break;
+ }
+
+ case 'm':
+ case 'M': {
+ if (selected_task == NULL) {
+ break;
+ }
+
+ intmax_t value;
+ if (read_input_to_int(selected_task_row, action_style, " Move to: ", &value) == false) {
+ break;
+ }
+
+ ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1;
+ move_task_to_index(db, selected_task, target_index);
+ trigger_autosave();
+ break;
+ }
+
+ case 'g':
+ case 'G': {
+ if (selected_task == NULL) {
+ break;
+ }
+
+ intmax_t value;
+ if (read_input_to_int(selected_task_row, action_style, " Go to: ", &value) == false) {
+ break;
+ }
+
+ ptrdiff_t target_index = (value < 1 ? 1 : value > MAX_DATABASE_TASKS ? MAX_DATABASE_TASKS : value) - 1;
+ select_task_by_index(db, target_index);
+ break;
+ }
+
+ case 'd':
+ case 'D':{
+ if (selected_task == NULL) {
+ break;
+ }
+
+ if (is_database_full(db)) {
+ read_enter_confirmation(selected_task_row, error_style, " Unable to duplicate entry: database is full. ");
+ break;
+ }
+
+ if (duplicate_task(db, selected_task) == false) {
+ break;
+ }
+ trigger_autosave();
+ break;
+ }
+
+ case KEY_F(5): {
+ update_total_times(db);
+ trigger_autosave();
+ break;
+ }
+
+ case 't':
+ case 'T': {
+ if (active_task == NULL) {
+ break;
+ }
+ select_task(db, active_task);
+ break;
+ }
+
+ case '\n':
+ case ' ': {
+ if (db != &database || selected_task == NULL) {
+ break;
+ }
+ set_active_task(db, (active_task == selected_task) ? NULL : selected_task);
+ active_task = get_active_task(db);
+ trigger_autosave();
+ break;
+ }
+
+ case '\t': {
+ if (db == &database) {
+ if (import_from_csv(&archive, ar_file_path) == false) {
+ reset_database(&archive);
+ print_error("Failed to load archive.");
+ break;
+ }
+ db = &archive;
+ }
+ else {
+ if (export_to_csv(&archive, ar_file_path) == false) {
+ print_error("Failed to store archive.");
+ break;
+ }
+ reset_database(&archive);
+ db = &database;
+ }
+ break;
+ }
+
+ case 'a':
+ case 'A': {
+ if (db != &database || selected_task == NULL || selected_task == active_task) {
+ break;
+ }
+ if (append_to_csv(selected_task, ar_file_path) == false) {
+ print_error("Failed to archive entry.");
+ break;
+ }
+ delete_task(&database, selected_task);
+ trigger_autosave();
+ break;
+ }
+
+ case 'u':
+ case 'U': {
+ if (db != &archive || selected_task == NULL) {
+ break;
+ }
+ if (is_database_full(&database)) {
+ read_enter_confirmation(selected_task_row, error_style, " Unable to unarchive entry: database is full. ");
+ break;
+ }
+ if (duplicate_task(&database, selected_task) == false) {
+ print_error("Failed to unarchive entry.");
+ break;
+ }
+ delete_task(&archive, selected_task);
+ trigger_autosave();
+ break;
+ }
+
+ case KEY_HOME: {
+ select_task_by_index(db, 0);
+ break;
+ }
+
+ case KEY_UP: {
+ select_task_by_delta(db, -1);
+ break;
+ }
+
+ case KEY_PPAGE: {
+ select_task_by_delta(db, -layout_tasks_rows);
+ break;
+ }
+
+ case KEY_END: {
+ select_task_by_index(db, db->count-1);
+ break;
+ }
+
+ case KEY_DOWN: {
+ select_task_by_delta(db, 1);
+ break;
+ }
+
+ case KEY_NPAGE: {
+ select_task_by_delta(db, layout_tasks_rows);
+ break;
+ }
+ }
+
+ if (is_terminal_too_small) {
+ const char *INVALID_WINDOW_MESSAGE = "Terminal is too small: minimum 60x3.";
+ const int INVALID_WINDOW_MESSAGE_LENGTH = strlen(INVALID_WINDOW_MESSAGE);
+ mvaddstr(size_y / 2, (size_x - INVALID_WINDOW_MESSAGE_LENGTH) / 2, INVALID_WINDOW_MESSAGE);
+ }
+ else {
+ draw_tui(db, layout);
+ draw_error_window();
+ }
+
+ timeout(INPUT_TIMEOUT_MS);
+ }
+
+ // Save any unsaved changes.
+ show_processing();
+ bool error_saving = false;
+ if (db == &archive) {
+ if (export_to_csv(&archive, ar_file_path) == false) {
+ print_error("Failed to save archive.");
+ error_saving |= true;
+ }
+ }
+ if (countdown_to_autosave > 0 || is_autosave_enabled == false) {
+ if (store_database(&database, db_file_path) == false) {
+ print_error("Failed to save database.");
+ error_saving |= true;
+ }
+ }
+ if (error_saving) {
+ print_error("Press any key to close.");
+ draw_error_window();
+ timeout(INPUT_AWAIT_INF);
+ getch();
+ }
+
+ endwin();
+ free_memory();
+ return error_saving ? EXIT_FAILURE : EXIT_SUCCESS;
}