add cnfind command
This commit is contained in:
parent
b68b09b777
commit
44f48c2f76
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
10
Makefile
10
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
|
||||
|
||||
28
README.md
28
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
|
||||
|
||||
@ -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
342
src/cnfind.c
Normal 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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user