cnotes/src/cnfind.c

343 lines
9.7 KiB
C
Raw Normal View History

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;
}
int get_cnotes_path(char *buffer, size_t bufsize) {
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(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;
}
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(CNOTES_FILE) + 2 > bufsize) {
fprintf(stderr, "Error: Path too long\n");
return 0;
}
sprintf(buffer, "%s" PATH_SEP_STR "%s", home, CNOTES_FILE);
} else {
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);
}
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(void) {
print_horizontal_line('+', '+', '+', '=');
printf("| %-3s | %-*s | %-*s | %-*s | %-*s |\n",
"Ln",
DATE_LENGTH, "Date",
TIME_LENGTH, "Time",
CATEGORY_LENGTH, "Category",
TXTMSG_LENGTH, "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, " -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);
}
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;
/* 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], "-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;
}
/* Get file path */
if (!get_cnotes_path(file_path, sizeof(file_path))) {
return 1;
}
notesfile = fopen(file_path, "r");
if (notesfile == NULL) {
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();
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.\n", match_count);
}
return (match_count > 0) ? 0 : 1;
}