add cnfind command

This commit is contained in:
Gregory Gauthier 2026-01-30 13:39:48 +00:00
parent b68b09b777
commit 44f48c2f76
7 changed files with 384 additions and 11 deletions

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
}
]

342
src/cnfind.c Normal file
View File

@ -0,0 +1,342 @@
/*
* 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;
}