diff --git a/BUILD.BAT b/BUILD.BAT index 07b719a..adfc493 100644 --- a/BUILD.BAT +++ b/BUILD.BAT @@ -26,6 +26,10 @@ ECHO Compiling cndel.exe... TCC -A -w -ml -I.\include -ebuild\cndel.exe src\cndel.c IF ERRORLEVEL 1 GOTO ERROR +ECHO Compiling cnfind.exe... +TCC -A -w -ml -I.\include -ebuild\cnfind.exe src\cnfind.c +IF ERRORLEVEL 1 GOTO ERROR + ECHO. ECHO Build successful! ECHO. diff --git a/CMakeLists.txt b/CMakeLists.txt index 5f18b71..6468a6c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,3 +24,4 @@ add_executable(cnadd src/cnadd.c) add_executable(cndump src/cndump.c) add_executable(cncount src/cncount.c) add_executable(cndel src/cndel.c) +add_executable(cnfind src/cnfind.c) diff --git a/MAKEFILE.TC b/MAKEFILE.TC index 3d88998..7abca8c 100644 --- a/MAKEFILE.TC +++ b/MAKEFILE.TC @@ -23,7 +23,7 @@ BUILDDIR = build .c.obj: $(CC) $(CFLAGS) -c -o$*.obj $< -all: $(BUILDDIR) $(BUILDDIR)\cnadd.exe $(BUILDDIR)\cndump.exe $(BUILDDIR)\cncount.exe $(BUILDDIR)\cndel.exe +all: $(BUILDDIR) $(BUILDDIR)\cnadd.exe $(BUILDDIR)\cndump.exe $(BUILDDIR)\cncount.exe $(BUILDDIR)\cndel.exe $(BUILDDIR)\cnfind.exe $(BUILDDIR): if not exist $(BUILDDIR) mkdir $(BUILDDIR) @@ -40,6 +40,9 @@ $(BUILDDIR)\cncount.exe: $(SRCDIR)\cncount.c $(INCDIR)\platform.h $(INCDIR)\conf $(BUILDDIR)\cndel.exe: $(SRCDIR)\cndel.c $(INCDIR)\platform.h $(INCDIR)\config.h $(CC) $(CFLAGS) -e$(BUILDDIR)\cndel.exe $(SRCDIR)\cndel.c +$(BUILDDIR)\cnfind.exe: $(SRCDIR)\cnfind.c $(INCDIR)\platform.h $(INCDIR)\config.h + $(CC) $(CFLAGS) -e$(BUILDDIR)\cnfind.exe $(SRCDIR)\cnfind.c + clean: if exist $(BUILDDIR)\*.exe del $(BUILDDIR)\*.exe if exist $(BUILDDIR)\*.obj del $(BUILDDIR)\*.obj diff --git a/Makefile b/Makefile index 955b9f6..6b0005b 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,10 @@ SRCDIR = src INCDIR = include BUILDDIR = build -SOURCES = $(SRCDIR)/cnadd.c $(SRCDIR)/cndump.c $(SRCDIR)/cncount.c $(SRCDIR)/cndel.c +SOURCES = $(SRCDIR)/cnadd.c $(SRCDIR)/cndump.c $(SRCDIR)/cncount.c $(SRCDIR)/cndel.c $(SRCDIR)/cnfind.c HEADERS = $(INCDIR)/platform.h $(INCDIR)/config.h -TARGETS = $(BUILDDIR)/cnadd $(BUILDDIR)/cndump $(BUILDDIR)/cncount $(BUILDDIR)/cndel +TARGETS = $(BUILDDIR)/cnadd $(BUILDDIR)/cndump $(BUILDDIR)/cncount $(BUILDDIR)/cndel $(BUILDDIR)/cnfind .PHONY: all clean install uninstall @@ -33,6 +33,9 @@ $(BUILDDIR)/cncount: $(SRCDIR)/cncount.c $(HEADERS) $(BUILDDIR)/cndel: $(SRCDIR)/cndel.c $(HEADERS) $(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cndel.c +$(BUILDDIR)/cnfind: $(SRCDIR)/cnfind.c $(HEADERS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cnfind.c + clean: rm -rf $(BUILDDIR) @@ -42,6 +45,7 @@ install: $(TARGETS) install -m 755 $(BUILDDIR)/cndump /usr/local/bin/ install -m 755 $(BUILDDIR)/cncount /usr/local/bin/ install -m 755 $(BUILDDIR)/cndel /usr/local/bin/ + install -m 755 $(BUILDDIR)/cnfind /usr/local/bin/ uninstall: - rm -f /usr/local/bin/cnadd /usr/local/bin/cndump /usr/local/bin/cncount /usr/local/bin/cndel + rm -f /usr/local/bin/cnadd /usr/local/bin/cndump /usr/local/bin/cncount /usr/local/bin/cndel /usr/local/bin/cnfind diff --git a/README.md b/README.md index 4e9bd82..c7b5c7e 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,27 @@ Options: - `-l` - Delete the last (most recent) entry - `-y` - Skip confirmation prompt +### cnfind + +Search notes by text, category, or date. + +```bash +cnfind meeting # search for 'meeting' in text (case-insensitive) +cnfind -c Work # show all entries in Work category +cnfind -d 2025-01-30 # show all entries from a specific date +cnfind -c Work meeting # search 'meeting' in Work category only +cnfind -i meeting # case-sensitive search +cnfind -n meeting # show only match count +``` + +Options: +- `-c CATEGORY` - Filter by category (case-insensitive) +- `-d DATE` - Filter by date (YYYY-MM-DD) +- `-i` - Case-sensitive search (default is case-insensitive) +- `-n` - Show only count of matches + +The output includes line numbers for use with `cndel -n`. + ## Configuration ### Notes Location @@ -155,13 +176,6 @@ Fields: Future commands to implement: -- [ ] **cnfind** - Search notes by keyword, category, or date - ``` - cnfind "meeting" # search in message text - cnfind -c Work # filter by category - cnfind -d 2025-01-30 # filter by date - ``` - - [ ] **cntail** - Show last N entries (quick view) ``` cntail # last 5 entries diff --git a/compile_commands.json b/compile_commands.json index 9265138..3f7dd52 100644 --- a/compile_commands.json +++ b/compile_commands.json @@ -18,5 +18,10 @@ "directory": ".", "command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cndel.c -o build/cndel.o", "file": "src/cndel.c" + }, + { + "directory": ".", + "command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cnfind.c -o build/cnfind.o", + "file": "src/cnfind.c" } ] diff --git a/src/cnfind.c b/src/cnfind.c new file mode 100644 index 0000000..0731478 --- /dev/null +++ b/src/cnfind.c @@ -0,0 +1,342 @@ +/* + * 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 *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; +}