2026-01-30 13:39:48 +00:00
|
|
|
/*
|
|
|
|
|
* cnfind - Search note entries
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <ctype.h>
|
|
|
|
|
#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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:51:49 +00:00
|
|
|
int get_cnotes_path(char *buffer, size_t bufsize, const char *filename) {
|
2026-01-30 13:39:48 +00:00
|
|
|
const char *custom_path;
|
|
|
|
|
const char *home;
|
|
|
|
|
size_t len;
|
|
|
|
|
|
|
|
|
|
custom_path = getenv(CNOTES_PATH_ENV);
|
|
|
|
|
if (custom_path != NULL && strlen(custom_path) > 0) {
|
2026-01-30 14:51:49 +00:00
|
|
|
if (strlen(custom_path) + strlen(filename) + 2 > bufsize) {
|
2026-01-30 13:39:48 +00:00
|
|
|
fprintf(stderr, "Error: Path too long\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-01-30 14:51:49 +00:00
|
|
|
sprintf(buffer, "%s" PATH_SEP_STR "%s", custom_path, filename);
|
2026-01-30 13:39:48 +00:00
|
|
|
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) {
|
2026-01-30 14:51:49 +00:00
|
|
|
if (len + strlen(filename) + 2 > bufsize) {
|
2026-01-30 13:39:48 +00:00
|
|
|
fprintf(stderr, "Error: Path too long\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-01-30 14:51:49 +00:00
|
|
|
sprintf(buffer, "%s" PATH_SEP_STR "%s", home, filename);
|
2026-01-30 13:39:48 +00:00
|
|
|
} else {
|
2026-01-30 14:51:49 +00:00
|
|
|
if (len + strlen(CNOTES_DIR) + strlen(filename) + 3 > bufsize) {
|
2026-01-30 13:39:48 +00:00
|
|
|
fprintf(stderr, "Error: Path too long\n");
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-01-30 14:51:49 +00:00
|
|
|
sprintf(buffer, "%s" PATH_SEP_STR "%s" PATH_SEP_STR "%s", home, CNOTES_DIR, filename);
|
2026-01-30 13:39:48 +00:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:51:49 +00:00
|
|
|
void print_header(int is_archive) {
|
2026-01-30 13:39:48 +00:00
|
|
|
print_horizontal_line('+', '+', '+', '=');
|
|
|
|
|
printf("| %-3s | %-*s | %-*s | %-*s | %-*s |\n",
|
|
|
|
|
"Ln",
|
|
|
|
|
DATE_LENGTH, "Date",
|
|
|
|
|
TIME_LENGTH, "Time",
|
|
|
|
|
CATEGORY_LENGTH, "Category",
|
2026-01-30 14:51:49 +00:00
|
|
|
TXTMSG_LENGTH, is_archive ? "Free Text (ARCHIVED)" : "Free Text");
|
2026-01-30 13:39:48 +00:00
|
|
|
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");
|
2026-01-30 14:51:49 +00:00
|
|
|
fprintf(stderr, " -a, --archive Search archived entries instead of active\n");
|
2026-01-30 13:39:48 +00:00
|
|
|
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);
|
2026-01-30 14:51:49 +00:00
|
|
|
fprintf(stderr, " %s -a meeting Search archived entries\n", prog_name);
|
2026-01-30 13:39:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-01-30 14:51:49 +00:00
|
|
|
int search_archive = 0;
|
|
|
|
|
const char *target_file;
|
2026-01-30 13:39:48 +00:00
|
|
|
|
|
|
|
|
/* 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;
|
|
|
|
|
}
|
2026-01-30 14:51:49 +00:00
|
|
|
else if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--archive") == 0) {
|
|
|
|
|
search_archive = 1;
|
|
|
|
|
}
|
2026-01-30 13:39:48 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:51:49 +00:00
|
|
|
/* Select target file */
|
|
|
|
|
target_file = search_archive ? CNOTES_ARCHIVE_FILE : CNOTES_FILE;
|
|
|
|
|
|
2026-01-30 13:39:48 +00:00
|
|
|
/* Get file path */
|
2026-01-30 14:51:49 +00:00
|
|
|
if (!get_cnotes_path(file_path, sizeof(file_path), target_file)) {
|
2026-01-30 13:39:48 +00:00
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notesfile = fopen(file_path, "r");
|
|
|
|
|
if (notesfile == NULL) {
|
2026-01-30 14:51:49 +00:00
|
|
|
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");
|
|
|
|
|
}
|
2026-01-30 13:39:48 +00:00
|
|
|
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) {
|
2026-01-30 14:51:49 +00:00
|
|
|
print_header(search_archive);
|
2026-01-30 13:39:48 +00:00
|
|
|
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 {
|
2026-01-30 14:51:49 +00:00
|
|
|
printf("\n%d match(es) found%s.\n", match_count, search_archive ? " in archive" : "");
|
2026-01-30 13:39:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (match_count > 0) ? 0 : 1;
|
|
|
|
|
}
|