2026-01-30 11:42:56 +00:00
|
|
|
/*
|
|
|
|
|
* Created by gmgauthier on 05/10/2025.
|
|
|
|
|
*/
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
2026-01-30 12:06:07 +00:00
|
|
|
#include "platform.h"
|
2026-01-30 12:37:28 +00:00
|
|
|
#include "config.h"
|
2025-10-04 13:54:38 +00:00
|
|
|
|
2025-10-12 19:22:01 +00:00
|
|
|
#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 TXTMSG_LENGTH 125 /* Max length of a text message */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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) */
|
|
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
|
|
|
|
|
typedef struct {
|
2025-10-12 19:22:01 +00:00
|
|
|
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 */
|
2025-10-04 13:54:38 +00:00
|
|
|
} Entry;
|
|
|
|
|
|
2025-10-12 19:22:01 +00:00
|
|
|
/* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
void print_horizontal_line(char left, char middle, char right, char fill) {
|
|
|
|
|
int i;
|
|
|
|
|
printf("%c", left);
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Date column */
|
|
|
|
|
for (i = 0; i < DATE_LENGTH + 2; i++) printf("%c", fill);
|
|
|
|
|
printf("%c", middle);
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Time column */
|
|
|
|
|
for (i = 0; i < TIME_LENGTH + 2; i++) printf("%c", fill);
|
|
|
|
|
printf("%c", middle);
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Category column */
|
|
|
|
|
for (i = 0; i < CATEGORY_LENGTH + 2; i++) printf("%c", fill);
|
|
|
|
|
printf("%c", middle);
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Free text column */
|
2025-10-12 19:22:01 +00:00
|
|
|
for (i = 0; i < TXTMSG_LENGTH + 2; i++) printf("%c", fill);
|
2025-10-04 13:54:38 +00:00
|
|
|
printf("%c\n", right);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void print_header(void) {
|
|
|
|
|
/* Top border (double line) */
|
|
|
|
|
print_horizontal_line('+', '+', '+', '=');
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Header row */
|
|
|
|
|
printf("| %-*s | %-*s | %-*s | %-*s |\n",
|
|
|
|
|
DATE_LENGTH, "Date",
|
|
|
|
|
TIME_LENGTH, "Time",
|
|
|
|
|
CATEGORY_LENGTH, "Category",
|
2025-10-12 19:22:01 +00:00
|
|
|
TXTMSG_LENGTH, "Free Text");
|
|
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Bottom border (double line) */
|
|
|
|
|
print_horizontal_line('+', '+', '+', '=');
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 19:22:01 +00:00
|
|
|
void print_footer(void) {
|
|
|
|
|
print_horizontal_line('+', '+', '+', '-');
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
void print_entry(const Entry *entry) {
|
|
|
|
|
printf("| %-*s | %-*s | %-*s | %-*s |\n",
|
|
|
|
|
DATE_LENGTH, entry->date,
|
|
|
|
|
TIME_LENGTH, entry->time,
|
|
|
|
|
CATEGORY_LENGTH, entry->category,
|
2025-10-12 19:22:01 +00:00
|
|
|
TXTMSG_LENGTH, entry->text);
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-12 19:22:01 +00:00
|
|
|
|
|
|
|
|
/* Parse a fixed-length field followed by a delimiter */
|
|
|
|
|
static const char *parse_fixed_field(const char *ptr, char *dest, int length, char delimiter) {
|
2026-01-30 12:06:07 +00:00
|
|
|
if ((int)strlen(ptr) < length) return NULL;
|
2025-10-12 19:22:01 +00:00
|
|
|
strncpy(dest, ptr, length);
|
|
|
|
|
dest[length] = '\0';
|
|
|
|
|
ptr += length;
|
|
|
|
|
if (*ptr != delimiter) return NULL;
|
|
|
|
|
return ptr + 1; /* skip delimiter */
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
|
|
|
|
|
2025-10-12 19:22:01 +00:00
|
|
|
/* 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++;
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
dest[i] = '\0';
|
|
|
|
|
if (*ptr != delimiter) return NULL;
|
|
|
|
|
return ptr + 1; /* skip delimiter */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int parse_line(const char *line, Entry *entry) {
|
|
|
|
|
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;
|
|
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int get_cnotes_path(char *buffer, size_t bufsize) {
|
2026-01-30 12:37:28 +00:00
|
|
|
const char *custom_path;
|
2025-10-04 13:54:38 +00:00
|
|
|
const char *home;
|
|
|
|
|
size_t len;
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2026-01-30 12:37:28 +00:00
|
|
|
/* First check for CNOTES_PATH environment variable override */
|
|
|
|
|
custom_path = getenv(CNOTES_PATH_ENV);
|
|
|
|
|
if (custom_path != NULL && strlen(custom_path) > 0) {
|
|
|
|
|
if (strlen(custom_path) + strlen(CNOTES_FILE) + 2 > bufsize) {
|
|
|
|
|
fprintf(stderr, "Error: Path too long\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
sprintf(buffer, "%s" PATH_SEP_STR "%s", custom_path, CNOTES_FILE);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Fall back to HOME_ENV + CNOTES_DIR */
|
2026-01-30 12:06:07 +00:00
|
|
|
home = getenv(HOME_ENV);
|
2025-10-04 13:54:38 +00:00
|
|
|
if (home == NULL) {
|
2026-01-30 12:06:07 +00:00
|
|
|
fprintf(stderr, "Error: %s environment variable not set\n", HOME_ENV);
|
2026-01-30 12:37:28 +00:00
|
|
|
fprintf(stderr, "Hint: Set %s to specify notes directory\n", CNOTES_PATH_ENV);
|
2025-10-04 13:54:38 +00:00
|
|
|
return 0;
|
|
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
|
2026-01-30 12:37:28 +00:00
|
|
|
/* Build path to cnotes file */
|
2025-10-04 13:54:38 +00:00
|
|
|
len = strlen(home);
|
2026-01-30 12:37:28 +00:00
|
|
|
if (strlen(CNOTES_DIR) == 0) {
|
|
|
|
|
/* DOS mode: file in home directory directly */
|
|
|
|
|
if (len + strlen(CNOTES_FILE) + 2 > bufsize) {
|
|
|
|
|
fprintf(stderr, "Error: Path too long\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
sprintf(buffer, "%s" PATH_SEP_STR "%s", home, CNOTES_FILE);
|
|
|
|
|
} else {
|
|
|
|
|
/* Unix/Windows: HOME/CNOTES_DIR/CNOTES_FILE */
|
|
|
|
|
if (len + strlen(CNOTES_DIR) + strlen(CNOTES_FILE) + 3 > bufsize) {
|
|
|
|
|
fprintf(stderr, "Error: Path too long\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
sprintf(buffer, "%s" PATH_SEP_STR "%s" PATH_SEP_STR "%s", home, CNOTES_DIR, CNOTES_FILE);
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-12 19:22:01 +00:00
|
|
|
/* start with a simple cat of the file */
|
2025-10-12 19:32:58 +00:00
|
|
|
int main(int argc, char *argv[]) {
|
2026-01-30 12:06:07 +00:00
|
|
|
int i;
|
2025-10-12 19:22:01 +00:00
|
|
|
FILE *notesfile;
|
2025-10-04 13:54:38 +00:00
|
|
|
char file_path[600];
|
2026-01-30 12:06:07 +00:00
|
|
|
Entry *entries;
|
2025-10-12 19:22:01 +00:00
|
|
|
int entry_count = 0;
|
|
|
|
|
char line[MAX_LENGTH +1];
|
2026-01-30 12:06:07 +00:00
|
|
|
char *filename = NULL;
|
2025-10-12 19:22:01 +00:00
|
|
|
SortMode sort_mode = SORT_NONE;
|
2026-01-30 12:06:07 +00:00
|
|
|
|
|
|
|
|
/* Insures that we won't run out of memory in DOS */
|
|
|
|
|
entries = (Entry *)malloc(MAX_ENTRIES * sizeof(Entry));
|
|
|
|
|
if (entries == NULL) {
|
|
|
|
|
fprintf(stderr, "Error: Cannot allocate memory\n");
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
|
|
|
|
|
/* 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) {
|
2026-01-30 12:37:28 +00:00
|
|
|
fprintf(stderr, "cndump: cannot specify multiple sort options\n");
|
|
|
|
|
fprintf(stderr, "usage: cndump [[-d|--date] | [-c|--category]] [-r|--reverse] [filename]\n");
|
2025-10-12 19:22:01 +00:00
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
sort_mode = SORT_DATE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--category") == 0) {
|
|
|
|
|
if (sort_mode != SORT_NONE) {
|
2026-01-30 12:37:28 +00:00
|
|
|
fprintf(stderr, "cndump: cannot specify multiple sort options\n");
|
|
|
|
|
fprintf(stderr, "usage: cndump [[-d|--date] | [-c|--category]] [-r|--reverse] [filename]\n");
|
2025-10-12 19:22:01 +00:00
|
|
|
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];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 12:06:07 +00:00
|
|
|
/* TODO: filename argument is parsed but not yet used */
|
|
|
|
|
(void)filename;
|
|
|
|
|
|
2025-10-04 13:54:38 +00:00
|
|
|
/* Get cnotes file path */
|
|
|
|
|
if (!get_cnotes_path(file_path, sizeof(file_path))) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
|
|
|
|
|
notesfile = fopen(file_path, "r");
|
|
|
|
|
if (notesfile == NULL) {
|
2025-10-04 13:54:38 +00:00
|
|
|
fprintf(stderr, "Error: Cannot open file '%s'\n", file_path);
|
2026-01-30 12:37:28 +00:00
|
|
|
fprintf(stderr, "Hint: Use 'cnadd' to create your first entry\n");
|
2025-10-04 13:54:38 +00:00
|
|
|
return 1;
|
|
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
|
|
|
|
|
/* 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++;
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
else {
|
2026-01-30 12:37:28 +00:00
|
|
|
fprintf(stderr, "cndump: invalid line: %s\n", line);
|
2025-10-12 19:22:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fclose(notesfile);
|
|
|
|
|
|
|
|
|
|
/* Sort if requested */
|
|
|
|
|
if (sort_mode == SORT_DATE) {
|
|
|
|
|
qsort(entries, entry_count, sizeof(Entry), compare_by_date);
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
else if (sort_mode == SORT_CATEGORY) {
|
|
|
|
|
qsort(entries, entry_count, sizeof(Entry), compare_by_category);
|
2025-10-04 13:54:38 +00:00
|
|
|
}
|
2025-10-12 19:22:01 +00:00
|
|
|
|
|
|
|
|
/* Print the table */
|
|
|
|
|
print_header();
|
|
|
|
|
for (i = 0; i < entry_count; i++) {
|
|
|
|
|
print_entry(&entries[i]);
|
|
|
|
|
}
|
|
|
|
|
print_footer();
|
|
|
|
|
|
2026-01-30 12:06:07 +00:00
|
|
|
free(entries);
|
2025-10-04 13:54:38 +00:00
|
|
|
return 0;
|
2026-01-30 12:06:07 +00:00
|
|
|
}
|