diff options
Diffstat (limited to 'main.c')
| -rw-r--r-- | main.c | 708 |
1 files changed, 708 insertions, 0 deletions
@@ -0,0 +1,708 @@ +// Compilation command: +// - release: gcc main.c -Wall -O2 -m64 -lncurses -o ttt +// - debug : gcc main.c -Wall -g3 -m64 -lncurses -o ttt + + +#include <assert.h> +#include <errno.h> +#include <inttypes.h> +#include <ncurses.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#define STR_(X) #X // Convert to string. +#define STR(X) STR_(X) // Force argument expansions before converting to string. + +#define MAX_TASK_NAME 127 +#define FIRST_DAY_OF_WEEK 1 // (0-6, Sunday = 0) + +#define LOG_FILE_NAME "log.txt" +#define DB_BIN_PATH_NAME "./database.bin" +#define DB_CSV_PATH_NAME "./database.csv" + + + + +typedef struct { + char name[MAX_TASK_NAME+1]; // Allow space for null termination char. + uint32_t time[7]; + uint8_t state; +} task_t; + + +const char* DB_BIN_FILE_SIGNATURE = "TTBF:1.0"; +const uint8_t DB_BIN_FILE_SIGNATURE_SIZE = sizeof(DB_BIN_FILE_SIGNATURE); +const size_t SIZEOF_TASK_T = sizeof(task_t); +const char* DAYS_OF_WEEK[] = { "sun", "mon", "tue", "wed", "thu", "fri", "sat" }; +const uint8_t DAYS_ON_WEEK = sizeof(DAYS_OF_WEEK)/sizeof(char*); + +task_t* tasks = NULL; +uint32_t tasks_count = 0; +uint32_t tasks_capacity = 0; +uint32_t selected_task = -1; + + +// Given an UTF8 encoded string, truncate it to length without breaking any UTF8 character. +// The string should have, at least length number of items. +// The terminating null byte ('\0') is included in length. +// The function returns the amount of items that got discarded counting from length. +size_t truncate_string_utf8(uint8_t* string, size_t length) { + + size_t idx = length - 1; + + // Check for special case where no truncation is required. + if (string[idx] == '\0') { + return 0; + } + + // Search for a non-UTF8-sequence-item so we can truncate the string. + while(idx > 0 && ((string[idx] & 0xC0) == 0x80)) { + idx--; + } + + string[idx] = '\0'; + +#if DEBUG + // TODO Check if there is a null byte before the place where we terminated the string. + size_t idx_dbd = idx - 1; + while(idx_dbd >= 0) { + assert(string[idx_dbd] != '\0' && "Found unexpected null byte. The user is at fault."); + idx_dbd--; + } +#endif + + return length - idx; +} + +char* replace_char(char* string, char find, char replace){ + char *idx = string; + while((idx = strchr(idx, find)) != NULL) { + *idx = replace; + *idx++; + } + return string; +} + +void print_task(const task_t* task) { + printf("name: '%s'\n", task->name); + printf("t[0]: '%" PRIu32 "'\n", task->time[0]); + printf("t[1]: '%" PRIu32 "'\n", task->time[1]); + printf("t[2]: '%" PRIu32 "'\n", task->time[2]); + printf("t[3]: '%" PRIu32 "'\n", task->time[3]); + printf("t[4]: '%" PRIu32 "'\n", task->time[4]); + printf("t[5]: '%" PRIu32 "'\n", task->time[5]); + printf("t[6]: '%" PRIu32 "'\n", task->time[6]); +} + +void store_database(const task_t* database, uint32_t number_of_entries, const uint8_t* path_name) { + + assert(database != NULL); + assert(path_name != NULL); + + // Open file. + FILE* file = fopen(path_name, "w"); + if (file == NULL) { + fprintf(stderr, "Failed to open database file: %s.\n", strerror(errno)); // TODO Fix message. + return; + } + + fwrite(DB_BIN_FILE_SIGNATURE, sizeof(char), DB_BIN_FILE_SIGNATURE_SIZE, file); + fwrite(&number_of_entries, sizeof(number_of_entries), 1, file); + fwrite(database, SIZEOF_TASK_T, number_of_entries, file); + + fclose(file); +} + +uint32_t load_database(task_t** database, const char* path_name) { + + assert(database != NULL); + assert(path_name != NULL); + + // Open file. + FILE* file = fopen(path_name, "r"); + if (file == NULL) { + fprintf(stderr, "Failed to open database file '%s': %s.\n", path_name, strerror(errno)); // TODO Fix message. + return 0; + } + + // Validate file signature. + char file_signature[DB_BIN_FILE_SIGNATURE_SIZE]; + fread(&file_signature, sizeof(char), DB_BIN_FILE_SIGNATURE_SIZE, file); + if (strncmp(file_signature, DB_BIN_FILE_SIGNATURE, DB_BIN_FILE_SIGNATURE_SIZE) != 0) { + fprintf(stderr, "Invalid file signature.\n"); + return 0; + } + + // Read number of entries. + uint32_t number_of_entries; + fread(&number_of_entries, sizeof(number_of_entries), 1, file); + + // Read database entries. + *database = calloc(number_of_entries, SIZEOF_TASK_T); + fread(*database, SIZEOF_TASK_T, number_of_entries, file); + + // Make sure we are reading all the file. + assert(fgetc(file) == EOF); + + fclose(file); + + return number_of_entries; +} + +void export_database(const task_t* database, uint32_t number_of_entries, const char* path_name) { + + assert(database != NULL); + assert(path_name != NULL); + + FILE* file = fopen(path_name, "w"); + if (file == NULL) { + fprintf(stderr, "Failed to open database file '%s': %s.\n", path_name, strerror(errno)); // TODO Fix message. + return; + } + + fprintf(file, "%s,%s,%s,%s,%s,%s,%s,%s\n", + "task", + DAYS_OF_WEEK[0], + DAYS_OF_WEEK[1], + DAYS_OF_WEEK[2], + DAYS_OF_WEEK[3], + DAYS_OF_WEEK[4], + DAYS_OF_WEEK[5], + DAYS_OF_WEEK[6] + ); + + for (uint32_t idx = 0; idx < number_of_entries; idx++) { + const task_t* task = &database[idx]; + fprintf(file, "%s,%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 ",%" PRIu32 "\n", + task->name, + task->time[0], + task->time[1], + task->time[2], + task->time[3], + task->time[4], + task->time[5], + task->time[6] + ); + } + + fclose(file); +} + +uint32_t import_database(task_t** database, const char* path_name) { + + assert(database != NULL); + assert(path_name != NULL); + + FILE* file = fopen(path_name, "r"); + if (file == NULL) { + fprintf(stderr, "Failed to open database file '%s': %s.\n", path_name, strerror(errno)); // TODO Fix message. + return 0; + } + + uint32_t number_of_entries = 0; + uint32_t capacity = 0; + task_t task; + int error; + + fscanf(file, "%*[^\n]\n"); // Skip header line. + scan_csv: { + + error = fscanf(file, + "%" STR(MAX_TASK_NAME) "[^,],%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 ",%" SCNu32 "\n", + task.name, + &task.time[0], + &task.time[1], + &task.time[2], + &task.time[3], + &task.time[4], + &task.time[5], + &task.time[6] + ); + + if (error != EOF) + { + number_of_entries++; + + // Expand tasks capacity if required. + if (number_of_entries > capacity) { + capacity = capacity == 0 ? 16 : capacity << 1; + *database = realloc(*database, capacity * SIZEOF_TASK_T); + if (errno != 0 || errno == ENOMEM) { + ; // TODO realloc failed + } + } + memcpy(&((*database)[number_of_entries-1]), &task, SIZEOF_TASK_T); + + goto scan_csv; + } + } + + assert(number_of_entries == 3); + *database = realloc(*database, number_of_entries * SIZEOF_TASK_T); + // TODO check for allocation errors. +// if (error != EOF) { +// fprintf(stderr, "Failed to parse database file: %s.\n", strerror(errno)); +// } + + + fclose(file); + + return number_of_entries; +} + + +void prt(uint8_t* str, uint8_t size) { // TODO Debug function... to be removed. + fprintf(stderr, ">"); + int count = 0; + for (uint8_t idx = 0; idx < size; idx++) { + if (idx % 2 == 0) { + fprintf(stderr, " "); + } + fprintf(stderr, "%02x", str[idx]); + } + fprintf(stderr, "\n"); +} + +void prototype() { + + int count; + uint8_t size = 20; + uint8_t* test_string; + test_string = calloc(size, sizeof(uint8_t)); + fprintf(stderr, "%s", "çéº𒐫\n"); // C3 A7 - C3 A9 - C2 BA - F0 92 90 AB + sprintf(test_string, "%s", "çéº𒐫\nxpto "); + + prt(test_string, size); +// test_string[10] = '\0'; +// prt(test_string, size); + uint8_t trunc = truncate_string_utf8(test_string, 3); + prt(test_string, size); + fprintf(stderr, ">>> %d : '%s'\n", trunc, test_string); + + + FILE* file = fopen("./test.txt", "w"); + + uint8_t length = strlen(test_string); + fwrite(test_string, sizeof(uint8_t), length, file); + +// fwrite(test_string, sizeof(uint8_t), size, file); + + fclose(file); +// truncate_string(test_string, 8); +// fprintf(stderr, "%s", (char*)test_string); + return; + + + + const char* done = "# -- done -- -- -- /\n"; + + /////////////////////////////////////////////////////////////////////////// + // Get current UTC time. + + fprintf(stderr, "# current UTC time --------------------------- \\\n"); + time_t x = time(NULL); + printf("%s", ctime(&x)); + fprintf(stderr, done); + + + /////////////////////////////////////////////////////////////////////////// + // Get current day of the week. + + fprintf(stderr, "# current day of the week --------------------- \\\n"); + time_t now_ut = time(NULL); + uint8_t week_day = localtime(&now_ut)->tm_wday; + fprintf(stderr, "%d\n", week_day); + fprintf(stderr, done); + + + /////////////////////////////////////////////////////////////////////////// + // Prepare some data for testing. + + tasks_count = 3; + tasks = calloc(tasks_count, SIZEOF_TASK_T); + + task_t tmp[] = { + { + .name = "TASK-00 : Sample task name.", + .time = { 3,2,1,0,1,2,3 }, + }, + { + .name = "TASK-01 : A wild task appears", + .time = { 1,2,3,4,5,6,7 }, + }, + { + .name = "BAZINGA : Not a task!", + .time = { 11,22,33,44,55,66,77 }, + } + }; + memcpy(tasks, &tmp, SIZEOF_TASK_T * tasks_count); + + + /////////////////////////////////////////////////////////////////////////// + // Store database from memory to binary file. + + fprintf(stderr, "# store database ------------------------------ \\\n"); + store_database(tasks, tasks_count, DB_BIN_PATH_NAME); + fprintf(stderr, done); + + + /////////////////////////////////////////////////////////////////////////// + // Load database from binary file to memory. + + fprintf(stderr, "# load database ------------------------------- \\\n"); + task_t* task_load = NULL; + uint32_t number_of_entries_load = load_database(&task_load, DB_BIN_PATH_NAME); + + fprintf(stderr, "loaded %" PRIu32 " entries.\n", number_of_entries_load); + for (uint32_t idx = 0; idx < number_of_entries_load; idx++) { + print_task(&task_load[idx]); + } + free(task_load); + fprintf(stderr, done); + + + /////////////////////////////////////////////////////////////////////////// + // Export database to CSV file. + + fprintf(stderr, "# export to CSV ------------------------------- \\\n"); + export_database(tasks, tasks_count, DB_CSV_PATH_NAME); + fprintf(stderr, done); + + + /////////////////////////////////////////////////////////////////////////// + // Import database from CSV file. + + fprintf(stderr, "# import from CSV ----------------------------- \\\n"); + task_t* task_import = NULL; + uint32_t number_of_entries_import = import_database(&task_import, DB_CSV_PATH_NAME); + + fprintf(stderr, "imported %" PRIu32 " entries.\n", number_of_entries_import); + for (uint32_t idx = 0; idx < number_of_entries_import; idx++) { + print_task(&task_import[idx]); + } + free(task_import); + fprintf(stderr, done); + + + /////////////////////////////////////////////////////////////////////////// + // Release memory and exit. + +// free(tasks); +// return; +} + + +char* line_buffer; +int size_x, size_y; +uint8_t selected_layout = 0; + +#define TABLE_HEADERS_SIZE 9 + +typedef struct { + int table_size; + int table_headers_size; + char* table_headers[TABLE_HEADERS_SIZE]; +} layout_t; + +layout_t* layouts = NULL; + + +void initialize_layouts() { + layout_t* layout = NULL; + + layouts = calloc(2, sizeof(layout_t)); + + // Layout : 0 : normal. + layout = &layouts[0]; + layout->table_size = 8; + layout->table_headers_size = TABLE_HEADERS_SIZE; + // TODO Headers must be dynamic according to FIRST_DAY_OF_WEEK + layout->table_headers[0] = " Task "; + layout->table_headers[1] = " Mon "; + layout->table_headers[2] = " Tue "; + layout->table_headers[3] = " Wed "; + layout->table_headers[4] = " Thu "; + layout->table_headers[5] = " Fri "; + layout->table_headers[6] = " Sat "; + layout->table_headers[7] = " Sun "; + layout->table_headers[8] = " Total "; +// layout->table_headers = { " Task ", " Mon ", " Tue ", " Wed ", " Thu ", " Fri ", " Sat ", " Sun ", " Total " }; + + // Layout : 1 : compact. + layout = &layouts[1]; + layout->table_size = 6; + layout->table_headers_size = TABLE_HEADERS_SIZE; + // TODO Headers must be dynamic according to FIRST_DAY_OF_WEEK + layout->table_headers[0] = " Task "; + layout->table_headers[1] = " M "; + layout->table_headers[2] = " T "; + layout->table_headers[3] = " W "; + layout->table_headers[4] = " T "; + layout->table_headers[5] = " F "; + layout->table_headers[6] = " S "; + layout->table_headers[7] = " S "; + layout->table_headers[8] = " # "; +// layout->table_headers = { " T ", " M ", " T ", " W ", " T ", " F ", " S ", " S ", " # " }; +} + +void free_memory() { + free(line_buffer); + free(tasks); // TODO Deallocate tasks. + free(layouts); +} + +void draw_header() { + + layout_t* layout = &layouts[selected_layout]; + int table_size = layout->table_size; + char** table_headers = layout->table_headers; + + mvaddch(0, 0, ACS_ULCORNER); + for (int idx = 1; idx < size_x-1; idx++) { + addch(ACS_HLINE); + } + addch(ACS_URCORNER); + + int start_columns = size_x - 1 - 8*table_size - 2; + int columns[] = { + 0, + start_columns+(table_size*0), + start_columns+(table_size*1), + start_columns+(table_size*2), + start_columns+(table_size*3), + start_columns+(table_size*4), + start_columns+(table_size*5), + start_columns+(table_size*6), + start_columns+(table_size*7), + }; + + mvaddstr(0, columns[0]+2, table_headers[0]); + for (int idx = 1; idx < 9; idx++) { + mvaddch(0, columns[idx], ACS_TTEE); + mvaddstr(0, columns[idx]+2, table_headers[idx]); + } +} + +void draw_table() { + task_t* task; + int x, y; + + layout_t* layout = &layouts[selected_layout]; + int table_size = layout->table_size; + char** table_headers = layout->table_headers; + + // TODO Colors + start_color(); + init_pair(1, COLOR_BLACK, COLOR_CYAN); + + + int start_columns = size_x - 1 - 8*table_size - 2; + int columns[] = { + 0, + start_columns+(table_size*0), + start_columns+(table_size*1), + start_columns+(table_size*2), + start_columns+(table_size*3), + start_columns+(table_size*4), + start_columns+(table_size*5), + start_columns+(table_size*6), + start_columns+(table_size*7), + size_x-1, + }; + + move(1, 0); + for (uint32_t idx = 0; idx < tasks_count; idx++){ + + task = &tasks[idx]; + + // Enable highlight color on selected entry. + if (idx == selected_task) { + attron(COLOR_PAIR(1)); + } + + // Clear line_buffer and add string termination. + memset(line_buffer, ' ', size_x * sizeof(char)); + line_buffer[size_x-1] = '\0'; + + // Check maximum available space for task name and print it accordingly. + size_t task_name_length = strlen(task->name)*sizeof(char); + size_t max_column_length = start_columns - 2; + size_t copy_length = task_name_length < max_column_length ? task_name_length : max_column_length; + memcpy(line_buffer+2, task->name, copy_length); + addstr(line_buffer); + + // Disable highlight color on selected entry. + if (idx == selected_task) { + attroff(COLOR_PAIR(1)); + } + + // Print columns separators. + getyx(stdscr,y,x); + for (int c_idx = 0; c_idx < sizeof(columns)/sizeof(int); c_idx++) { + mvaddch(y, columns[c_idx], ACS_VLINE); + } + + // Go to next line. + y++; + move(y, 0); + } +} + +void draw_footer() { + const char *app_name = "Task Tracker"; + const char *app_version = "v1.0"; + + int row, col; /* to store the number of rows and the number of colums of the screen */ + initscr(); /* start the curses mode */ + getmaxyx(stdscr,row,col); /* get the number of rows and columns */ + + mvaddch(row-1, 0, ACS_LLCORNER); + addch(' '); + printw(app_name); + addch(' '); + for (int idx = strlen(app_name) + 3; idx < col - strlen(app_version) - 3; idx ++) { + addch(ACS_HLINE); + } + addch(' '); + printw(app_version); + addch(' '); + addch(ACS_LRCORNER); +} + +WINDOW *create_newwin(int height, int width, int starty, int startx); +void destroy_win(WINDOW *local_win); + +int main(int argc, char *argv[]) { + + // TODO Parse commands using: https://stackoverflow.com/questions/9642732/parsing-command-line-arguments-in-c + if (argc > 1) { +// const char* command_version = { "--version", "-v" }; + + for (int idx = 1; idx < argc; idx++) { + if (strncmp(argv[idx], "-v", 2) == 0 || strncmp(argv[idx], "--version", 9) == 0) { + fprintf(stdout, "Task Time Tracker v1.0\n"); + return EXIT_SUCCESS; + } + } + + + fprintf(stdout, "Unkown command '%s'.\nUse '%s --help' for list of commands.\n", argv[1], argv[0]); + return EXIT_FAILURE; + } + + + + prototype(); // TODO + return EXIT_FAILURE; + initialize_layouts(); + + WINDOW *my_win; + int startx, starty, width, height; + int ch; + + 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. + + height = 3; + width = 10; + starty = (LINES - height) / 2; /* Calculating for a center placement */ + startx = (COLS - width) / 2; /* of the window */ + printw("Press F1 to exit"); + refresh(); + my_win = create_newwin(height, width, starty, startx); + + int rows; + int colums; + + ch = KEY_RESIZE; + do { + switch(ch) + { + case KEY_RESIZE: + erase(); + if (line_buffer != NULL) { + free(line_buffer); + } + getmaxyx(stdscr, size_y, size_x); + line_buffer = malloc(size_x * sizeof(char)); + break; + + case KEY_LEFT: + destroy_win(my_win); + my_win = create_newwin(height, width, starty,--startx); + break; + + case KEY_RIGHT: + destroy_win(my_win); + my_win = create_newwin(height, width, starty,++startx); + break; + + case KEY_UP: + selected_task = selected_task == 0 ? 0 : selected_task - 1; + destroy_win(my_win); + my_win = create_newwin(height, width, --starty,startx); + break; + + case KEY_DOWN: + selected_task = (selected_task+1) == tasks_count ? selected_task : selected_task + 1; + destroy_win(my_win); + my_win = create_newwin(height, width, ++starty,startx); + break; + } + + selected_layout = size_x > 100 ? 0 : 1; + + if (size_x >= 60 && size_y > 2) { + draw_header(); + draw_table(); + draw_footer(); + } + else { + const char* INVALID_WINDOW_MESSAGE = "Please expand window."; + mvaddstr(size_y / 2, (size_x - strlen(INVALID_WINDOW_MESSAGE)) / 2, INVALID_WINDOW_MESSAGE); + } + + } while((ch = getch()) != KEY_F(1)); + + free_memory(); + endwin(); + return EXIT_SUCCESS; +} + +WINDOW *create_newwin(int height, int width, int starty, int startx) { + WINDOW *local_win; + + local_win = newwin(height, width, starty, startx); + box(local_win, 0 , 0); /* 0, 0 gives default characters + * for the vertical and horizontal + * lines */ + wrefresh(local_win); /* Show that box */ + + return local_win; +} + +void destroy_win(WINDOW *local_win) { + /* box(local_win, ' ', ' '); : This won't produce the desired + * result of erasing the window. It will leave it's four corners + * and so an ugly remnant of window. + */ + wborder(local_win, ' ', ' ', ' ',' ',' ',' ',' ',' '); + /* The parameters taken are + * 1. win: the window on which to operate + * 2. ls: character to be used for the left side of the window + * 3. rs: character to be used for the right side of the window + * 4. ts: character to be used for the top side of the window + * 5. bs: character to be used for the bottom side of the window + * 6. tl: character to be used for the top left corner of the window + * 7. tr: character to be used for the top right corner of the window + * 8. bl: character to be used for the bottom left corner of the window + * 9. br: character to be used for the bottom right corner of the window + */ + wrefresh(local_win); + delwin(local_win); +} |
