/* * cnfind - Search note entries */ #include #include #include #include #include "platform.h" #include "config.h" #define MAX_LENGTH 500 #define DATE_LENGTH 10 #define TIME_LENGTH 5 #define TXTMSG_LENGTH 125 typedef struct { char date[DATE_LENGTH + 1]; char time[TIME_LENGTH + 1]; char category[CATEGORY_LENGTH + 1]; char text[TXTMSG_LENGTH + 1]; int line_num; } Entry; /* Case-insensitive substring search */ static char *stristr(const char *haystack, const char *needle) { const char *h; const char *n; const char *start; if (*needle == '\0') return (char *)haystack; for (start = haystack; *start != '\0'; start++) { h = start; n = needle; while (*h != '\0' && *n != '\0' && tolower((unsigned char)*h) == tolower((unsigned char)*n)) { h++; n++; } if (*n == '\0') return (char *)start; } return NULL; } /* Parse a line into an Entry struct */ static int parse_line(const char *line, Entry *entry) { const char *ptr = line; int i; /* Parse date */ if (strlen(line) < DATE_LENGTH) return 0; strncpy(entry->date, ptr, DATE_LENGTH); entry->date[DATE_LENGTH] = '\0'; ptr += DATE_LENGTH; if (*ptr != ',') return 0; ptr++; /* Parse time */ if (strlen(ptr) < TIME_LENGTH) return 0; strncpy(entry->time, ptr, TIME_LENGTH); entry->time[TIME_LENGTH] = '\0'; ptr += TIME_LENGTH; if (*ptr != ',') return 0; ptr++; /* Parse category (up to comma) */ i = 0; while (*ptr != ',' && *ptr != '\0' && i < CATEGORY_LENGTH) { entry->category[i++] = *ptr++; } entry->category[i] = '\0'; /* Trim trailing spaces */ while (i > 0 && entry->category[i - 1] == ' ') { entry->category[--i] = '\0'; } if (*ptr != ',') return 0; ptr++; /* Parse text (quoted) */ if (*ptr != '"') return 0; ptr++; i = 0; while (*ptr != '"' && *ptr != '\0' && i < TXTMSG_LENGTH) { entry->text[i++] = *ptr++; } entry->text[i] = '\0'; return 1; } int get_cnotes_path(char *buffer, size_t bufsize, const char *filename) { const char *custom_path; const char *home; size_t len; custom_path = getenv(CNOTES_PATH_ENV); if (custom_path != NULL && strlen(custom_path) > 0) { if (strlen(custom_path) + strlen(filename) + 2 > bufsize) { fprintf(stderr, "Error: Path too long\n"); return 0; } sprintf(buffer, "%s" PATH_SEP_STR "%s", custom_path, filename); return 1; } home = getenv(HOME_ENV); if (home == NULL) { fprintf(stderr, "Error: %s environment variable not set\n", HOME_ENV); fprintf(stderr, "Hint: Set %s to specify notes directory\n", CNOTES_PATH_ENV); return 0; } len = strlen(home); if (strlen(CNOTES_DIR) == 0) { if (len + strlen(filename) + 2 > bufsize) { fprintf(stderr, "Error: Path too long\n"); return 0; } sprintf(buffer, "%s" PATH_SEP_STR "%s", home, filename); } else { if (len + strlen(CNOTES_DIR) + strlen(filename) + 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, filename); } return 1; } void print_horizontal_line(char left, char middle, char right, char fill) { int i; printf("%c", left); /* Line number column */ for (i = 0; i < 5; i++) printf("%c", fill); printf("%c", middle); /* 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 < TXTMSG_LENGTH + 2; i++) printf("%c", fill); printf("%c\n", right); } void print_header(int is_archive) { print_horizontal_line('+', '+', '+', '='); printf("| %-3s | %-*s | %-*s | %-*s | %-*s |\n", "Ln", DATE_LENGTH, "Date", TIME_LENGTH, "Time", CATEGORY_LENGTH, "Category", TXTMSG_LENGTH, is_archive ? "Free Text (ARCHIVED)" : "Free Text"); print_horizontal_line('+', '+', '+', '='); } void print_footer(void) { print_horizontal_line('+', '+', '+', '-'); } void print_entry(const Entry *entry) { printf("| %3d | %-*s | %-*s | %-*s | %-*s |\n", entry->line_num, DATE_LENGTH, entry->date, TIME_LENGTH, entry->time, CATEGORY_LENGTH, entry->category, TXTMSG_LENGTH, entry->text); } void print_usage(const char *prog_name) { fprintf(stderr, "Usage: %s [OPTIONS] [PATTERN]\n", prog_name); fprintf(stderr, "\n"); fprintf(stderr, "Search notes by text, category, or date.\n"); fprintf(stderr, "\n"); fprintf(stderr, "Options:\n"); fprintf(stderr, " -c CATEGORY Filter by category (case-insensitive)\n"); fprintf(stderr, " -d DATE Filter by date (YYYY-MM-DD)\n"); fprintf(stderr, " -i Case-sensitive search (default is case-insensitive)\n"); fprintf(stderr, " -n Show only count of matches\n"); fprintf(stderr, " -a, --archive Search archived entries instead of active\n"); fprintf(stderr, " -h, --help Show this help message\n"); fprintf(stderr, "\n"); fprintf(stderr, "Examples:\n"); fprintf(stderr, " %s meeting Search for 'meeting' in text\n", prog_name); fprintf(stderr, " %s -c Work Show all entries in Work category\n", prog_name); fprintf(stderr, " %s -d 2025-01-30 Show all entries from a date\n", prog_name); fprintf(stderr, " %s -c Work meeting Search 'meeting' in Work category\n", prog_name); fprintf(stderr, " %s -a meeting Search archived entries\n", prog_name); } int main(int argc, char *argv[]) { int i; FILE *notesfile; char file_path[600]; char line[MAX_LENGTH + 1]; Entry entry; int line_num = 0; int match_count = 0; int header_printed = 0; /* Search options */ char *pattern = NULL; char *filter_category = NULL; char *filter_date = NULL; int case_sensitive = 0; int count_only = 0; int search_archive = 0; const char *target_file; /* Parse command line arguments */ for (i = 1; i < argc; i++) { if (strcmp(argv[i], "-c") == 0) { if (i + 1 >= argc) { fprintf(stderr, "cnfind: -c requires a category\n"); print_usage(argv[0]); return 1; } filter_category = argv[++i]; } else if (strcmp(argv[i], "-d") == 0) { if (i + 1 >= argc) { fprintf(stderr, "cnfind: -d requires a date (YYYY-MM-DD)\n"); print_usage(argv[0]); return 1; } filter_date = argv[++i]; if (strlen(filter_date) != DATE_LENGTH) { fprintf(stderr, "cnfind: invalid date format '%s' (use YYYY-MM-DD)\n", filter_date); return 1; } } else if (strcmp(argv[i], "-i") == 0) { case_sensitive = 1; } else if (strcmp(argv[i], "-n") == 0) { count_only = 1; } else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) { search_archive = 1; } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { print_usage(argv[0]); return 0; } else if (argv[i][0] == '-') { fprintf(stderr, "cnfind: unknown option '%s'\n", argv[i]); print_usage(argv[0]); return 1; } else { /* Positional argument is the search pattern */ pattern = argv[i]; } } /* Must have at least one filter criteria */ if (pattern == NULL && filter_category == NULL && filter_date == NULL) { fprintf(stderr, "cnfind: must specify a search pattern, -c CATEGORY, or -d DATE\n"); print_usage(argv[0]); return 1; } /* Select target file */ target_file = search_archive ? CNOTES_ARCHIVE_FILE : CNOTES_FILE; /* Get file path */ if (!get_cnotes_path(file_path, sizeof(file_path), target_file)) { return 1; } notesfile = fopen(file_path, "r"); if (notesfile == NULL) { if (search_archive) { fprintf(stderr, "No archived entries found.\n"); } else { fprintf(stderr, "Error: Cannot open file '%s'\n", file_path); fprintf(stderr, "Hint: Use 'cnadd' to create your first entry\n"); } return 1; } /* Search entries */ while (fgets(line, MAX_LENGTH, notesfile) != NULL) { int matches = 1; if (strlen(line) < DATE_LENGTH) continue; line_num++; if (!parse_line(line, &entry)) { continue; } entry.line_num = line_num; /* Check date filter */ if (filter_date != NULL) { if (strncmp(entry.date, filter_date, DATE_LENGTH) != 0) { matches = 0; } } /* Check category filter */ if (matches && filter_category != NULL) { if (stristr(entry.category, filter_category) == NULL) { matches = 0; } } /* Check text pattern */ if (matches && pattern != NULL) { if (case_sensitive) { if (strstr(entry.text, pattern) == NULL) { matches = 0; } } else { if (stristr(entry.text, pattern) == NULL) { matches = 0; } } } if (matches) { match_count++; if (!count_only) { if (!header_printed) { print_header(search_archive); header_printed = 1; } print_entry(&entry); } } } fclose(notesfile); if (!count_only && header_printed) { print_footer(); } /* Print summary */ if (count_only) { printf("%d\n", match_count); } else { printf("\n%d match(es) found%s.\n", match_count, search_archive ? " in archive" : ""); } return (match_count > 0) ? 0 : 1; }