diff --git a/addnote.c b/addnote.c index 82dc35d..986ba5d 100644 --- a/addnote.c +++ b/addnote.c @@ -8,7 +8,7 @@ #include "platform.h" #define CATEGORY_LENGTH 10 -#define FREETEXT_MAX_LENGTH 80 +#define FREETEXT_MAX_LENGTH 125 #define DEFAULT_CATEGORY "General" #define CNOTES_DIR ".local/share/cnotes" #define CNOTES_FILE "cnotes.csv" diff --git a/dumpnotes.c b/dumpnotes.c index 87313fc..387c494 100644 --- a/dumpnotes.c +++ b/dumpnotes.c @@ -1,194 +1,253 @@ +// +// Created by gmgauthier on 05/10/2025. +// + #include #include #include -#define MAX_LINE_LENGTH 256 -#define DATE_LENGTH 10 -#define TIME_LENGTH 5 -#define CATEGORY_LENGTH 10 -#define FREETEXT_MAX_LENGTH 80 +#define MAX_LENGTH 500 /* Allow fgets to read up to this many characters */ +#define DATE_LENGTH 10 /* YYYY-MM-DD */ +#define TIME_LENGTH 5 /* HH:MM */ +#define CATEGORY_LENGTH 10 /* CATEGORY__ */ +#define TXTMSG_LENGTH 125 /* Max length of a text message */ +#define MAX_ENTRIES 5000 /* Maximum number of entries to load */ + +#define CNOTES_FILE "notes.csv" #define CNOTES_DIR ".local/share/cnotes" -#define CNOTES_FILE "cnotes.csv" + + +typedef enum { + SORT_NONE, + SORT_DATE, + SORT_CATEGORY +} SortMode; + +/* Global variable to control sort order: 1 for ascending, -1 for descending */ +static int g_sort_order = -1; /* Default: descending (newest/Z-A first) */ + typedef struct { - char date[DATE_LENGTH + 1]; - char time[TIME_LENGTH + 1]; - char category[CATEGORY_LENGTH + 1]; - char freetext[FREETEXT_MAX_LENGTH + 1]; + char date[DATE_LENGTH +1]; /* YYYY-MM-DD\0 */ + char time[TIME_LENGTH +1]; /* HH:MM\0 */ + char category[CATEGORY_LENGTH +1]; /* CATEGORY__\0 */ + char text[TXTMSG_LENGTH +1]; /* Text message\0 */ } Entry; +/* Comparison function for sorting by date/time */ +static int compare_by_date(const void *a, const void *b) { + const Entry *entry_a = (const Entry *)a; + const Entry *entry_b = (const Entry *)b; + int date_cmp = strcmp(entry_a->date, entry_b->date); + if (date_cmp != 0) return date_cmp * g_sort_order; + return strcmp(entry_a->time, entry_b->time) * g_sort_order; +} + +/* Comparison function for sorting by category */ +static int compare_by_category(const void *a, const void *b) { + const Entry *entry_a = (const Entry *)a; + const Entry *entry_b = (const Entry *)b; + return strcmp(entry_b->category, entry_a->category) * g_sort_order; +} + /* ReSharper disable CppParameterMayBeConst */ void print_horizontal_line(char left, char middle, char right, char fill) { /* ReSharper restore CppParameterMayBeConst */ int i; printf("%c", left); - + /* Date column */ for (i = 0; i < DATE_LENGTH + 2; i++) printf("%c", fill); printf("%c", middle); - + /* Time column */ for (i = 0; i < TIME_LENGTH + 2; i++) printf("%c", fill); printf("%c", middle); - + /* Category column */ for (i = 0; i < CATEGORY_LENGTH + 2; i++) printf("%c", fill); printf("%c", middle); - + /* Free text column */ - for (i = 0; i < FREETEXT_MAX_LENGTH + 2; i++) printf("%c", fill); + for (i = 0; i < TXTMSG_LENGTH + 2; i++) printf("%c", fill); printf("%c\n", right); } void print_header(void) { /* Top border (double line) */ print_horizontal_line('+', '+', '+', '='); - + /* Header row */ printf("| %-*s | %-*s | %-*s | %-*s |\n", DATE_LENGTH, "Date", TIME_LENGTH, "Time", CATEGORY_LENGTH, "Category", - FREETEXT_MAX_LENGTH, "Free Text"); - + TXTMSG_LENGTH, "Free Text"); + /* Bottom border (double line) */ print_horizontal_line('+', '+', '+', '='); } +void print_footer(void) { + print_horizontal_line('+', '+', '+', '-'); +} + void print_entry(const Entry *entry) { printf("| %-*s | %-*s | %-*s | %-*s |\n", DATE_LENGTH, entry->date, TIME_LENGTH, entry->time, CATEGORY_LENGTH, entry->category, - FREETEXT_MAX_LENGTH, entry->freetext); + TXTMSG_LENGTH, entry->text); } -void print_footer(void) { - print_horizontal_line('+', '+', '+', '-'); + +/* Parse a fixed-length field followed by a delimiter */ +static const char *parse_fixed_field(const char *ptr, char *dest, int length, char delimiter) { + if (strlen(ptr) < length) return NULL; + strncpy(dest, ptr, length); + dest[length] = '\0'; + ptr += length; + if (*ptr != delimiter) return NULL; + return ptr + 1; /* skip delimiter */ +} + +/* Parse a variable-length field up to max_length, terminated by delimiter */ +/* Truncates if field exceeds max_length but continues to find delimiter */ +static const char *parse_variable_field(const char *ptr, char *dest, int max_length, char delimiter) { + int i = 0; + while (*ptr != '\0' && *ptr != delimiter) { + if (i < max_length) { + dest[i++] = *ptr; + } + ptr++; + } + dest[i] = '\0'; + if (*ptr != delimiter) return NULL; + return ptr + 1; /* skip delimiter */ } int parse_line(const char *line, Entry *entry) { - const char *ptr; - int i; - - ptr = line; - - /* Parse date (10 chars) */ - if (strlen(ptr) < DATE_LENGTH) return 0; - strncpy(entry->date, ptr, DATE_LENGTH); - entry->date[DATE_LENGTH] = '\0'; - ptr += DATE_LENGTH; - - /* Skip comma */ - if (*ptr != ',') return 0; - ptr++; - - /* Parse time (5 chars) */ - if (strlen(ptr) < TIME_LENGTH) return 0; - strncpy(entry->time, ptr, TIME_LENGTH); - entry->time[TIME_LENGTH] = '\0'; - ptr += TIME_LENGTH; - - /* Skip comma */ - if (*ptr != ',') return 0; - ptr++; - - /* Parse category (10 chars) */ - if (strlen(ptr) < CATEGORY_LENGTH) return 0; - strncpy(entry->category, ptr, CATEGORY_LENGTH); - entry->category[CATEGORY_LENGTH] = '\0'; - ptr += CATEGORY_LENGTH; - - /* Skip comma */ - if (*ptr != ',') return 0; - ptr++; - - /* Parse free text (quoted) */ - if (*ptr != '"') return 0; - ptr++; - - i = 0; - while (*ptr != '\0' && *ptr != '"' && i < FREETEXT_MAX_LENGTH) { - entry->freetext[i++] = *ptr++; - } - entry->freetext[i] = '\0'; - - /* Should end with quote */ - if (*ptr != '"') return 0; - + const char *charptr = line; + + /* Parse date field (fixed length) */ + charptr = parse_fixed_field(charptr, entry->date, DATE_LENGTH, ','); + if (!charptr) return 0; + + /* Parse time field (fixed length) */ + charptr = parse_fixed_field(charptr, entry->time, TIME_LENGTH, ','); + if (!charptr) return 0; + + /* Parse category field (variable length) */ + charptr = parse_variable_field(charptr, entry->category, CATEGORY_LENGTH, ','); + if (!charptr) return 0; + + /* Parse text field (variable length, quoted) */ + if (*charptr != '"') return 0; + charptr++; + charptr = parse_variable_field(charptr, entry->text, TXTMSG_LENGTH, '"'); + if (!charptr) return 0; + return 1; } int get_cnotes_path(char *buffer, size_t bufsize) { const char *home; size_t len; - + home = getenv("HOME"); if (home == NULL) { fprintf(stderr, "Error: HOME environment variable not set\n"); return 0; } - + /* Build path: $HOME/.local/share/cnotes/cnotes.csv */ len = strlen(home); if (len + strlen(CNOTES_DIR) + strlen(CNOTES_FILE) + 3 > bufsize) { fprintf(stderr, "Error: Path too long\n"); return 0; } - + sprintf(buffer, "%s/%s/%s", home, CNOTES_DIR, CNOTES_FILE); return 1; } -int main(void) { - FILE *file; +/* start with a simple cat of the file */ +main(int argc, char *argv[]) { + FILE *notesfile; char file_path[600]; - char line[MAX_LINE_LENGTH]; - Entry entry; - int line_count; - int first_entry; - + Entry entries[MAX_ENTRIES]; + int entry_count = 0; + char line[MAX_LENGTH +1]; + char *filename = CNOTES_FILE; + SortMode sort_mode = SORT_NONE; + int i; + + /* Parse command line arguments */ + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--date") == 0) { + if (sort_mode != SORT_NONE) { + fprintf(stderr, "dumpnotes: cannot specify multiple sort options\n"); + fprintf(stderr, "usage: dumpnotes [[-d|--date] | [-c|--category]] [-r|--reverse] [filename]\n"); + exit(1); + } + sort_mode = SORT_DATE; + } + + else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--category") == 0) { + if (sort_mode != SORT_NONE) { + fprintf(stderr, "dumpnotes: cannot specify multiple sort options\n"); + fprintf(stderr, "usage: dumpnotes [[-d|--date] | [-c|--category]] [-r|--reverse] [filename]\n"); + exit(1); + } + sort_mode = SORT_CATEGORY; + } + + else if (strcmp(argv[i], "-r") == 0 || strcmp(argv[i], "--reverse") == 0) { + g_sort_order = 1; /* Ascending order */ + } + + else { + filename = argv[i]; + } + } + /* Get cnotes file path */ if (!get_cnotes_path(file_path, sizeof(file_path))) { return 1; } - - file = fopen(file_path, "r"); - if (file == NULL) { + + notesfile = fopen(file_path, "r"); + if (notesfile == NULL) { fprintf(stderr, "Error: Cannot open file '%s'\n", file_path); fprintf(stderr, "Hint: Use 'addnote' to create your first entry\n"); return 1; } - - line_count = 0; - first_entry = 1; - - while (fgets(line, sizeof(line), file) != NULL) { - line_count++; - - /* Remove trailing newline */ - line[strcspn(line, "\n")] = '\0'; - - /* Skip empty lines */ - if (strlen(line) == 0) continue; - - if (parse_line(line, &entry)) { - if (first_entry) { - print_header(); - first_entry = 0; - } - print_entry(&entry); - } else { - fprintf(stderr, "Warning: Skipping malformed line %d\n", line_count); + + /* Read all entries into memory */ + while (fgets(line, MAX_LENGTH, notesfile) != NULL && entry_count < MAX_ENTRIES) { + if (parse_line(line, &entries[entry_count])) { + entry_count++; + } + else { + fprintf(stderr, "dumpnotes: invalid line: %s\n", line); } } - - if (!first_entry) { - print_footer(); - } else { - printf("No valid entries found.\n"); + fclose(notesfile); + + /* Sort if requested */ + if (sort_mode == SORT_DATE) { + qsort(entries, entry_count, sizeof(Entry), compare_by_date); } - - fclose(file); + else if (sort_mode == SORT_CATEGORY) { + qsort(entries, entry_count, sizeof(Entry), compare_by_category); + } + + /* Print the table */ + print_header(); + for (i = 0; i < entry_count; i++) { + print_entry(&entries[i]); + } + print_footer(); + return 0; -} +} \ No newline at end of file