From b68b09b7773afc125e1a863462cd168536dc275c Mon Sep 17 00:00:00 2001 From: Gregory Gauthier Date: Fri, 30 Jan 2026 13:35:14 +0000 Subject: [PATCH] add cncount and cndel commands --- BUILD.BAT | 8 + CMakeLists.txt | 26 ++++ MAKEFILE.TC | 8 +- Makefile | 14 +- README.md | 189 +++++++++++++++++++++++ compile_commands.json | 22 +++ src/cncount.c | 266 +++++++++++++++++++++++++++++++++ src/cndel.c | 340 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 869 insertions(+), 4 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 compile_commands.json create mode 100644 src/cncount.c create mode 100644 src/cndel.c diff --git a/BUILD.BAT b/BUILD.BAT index cc89eb8..07b719a 100644 --- a/BUILD.BAT +++ b/BUILD.BAT @@ -18,6 +18,14 @@ ECHO Compiling cndump.exe... TCC -A -w -ml -I.\include -ebuild\cndump.exe src\cndump.c IF ERRORLEVEL 1 GOTO ERROR +ECHO Compiling cncount.exe... +TCC -A -w -ml -I.\include -ebuild\cncount.exe src\cncount.c +IF ERRORLEVEL 1 GOTO ERROR + +ECHO Compiling cndel.exe... +TCC -A -w -ml -I.\include -ebuild\cndel.exe src\cndel.c +IF ERRORLEVEL 1 GOTO ERROR + ECHO. ECHO Build successful! ECHO. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..5f18b71 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 4.1) +project(cnotes C) + +# Force strict C90/ANSI compliance +set(CMAKE_C_STANDARD 90) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +# Strict warning flags to match Makefile +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -ansi -Wpedantic -Wall -Wextra") + +include_directories(include) + +# Platform-specific definitions for Unix (IDE/static analysis support) +# These match the defaults in platform.h for non-Windows/non-DOS builds +add_compile_definitions( + PATH_SEP_STR="/" + HOME_ENV="HOME" + CNOTES_PATH_ENV="CNOTES_PATH" +) + +# Separate executables +add_executable(cnadd src/cnadd.c) +add_executable(cndump src/cndump.c) +add_executable(cncount src/cncount.c) +add_executable(cndel src/cndel.c) diff --git a/MAKEFILE.TC b/MAKEFILE.TC index 642e5b2..3d88998 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 +all: $(BUILDDIR) $(BUILDDIR)\cnadd.exe $(BUILDDIR)\cndump.exe $(BUILDDIR)\cncount.exe $(BUILDDIR)\cndel.exe $(BUILDDIR): if not exist $(BUILDDIR) mkdir $(BUILDDIR) @@ -34,6 +34,12 @@ $(BUILDDIR)\cnadd.exe: $(SRCDIR)\cnadd.c $(INCDIR)\platform.h $(INCDIR)\config.h $(BUILDDIR)\cndump.exe: $(SRCDIR)\cndump.c $(INCDIR)\platform.h $(INCDIR)\config.h $(CC) $(CFLAGS) -e$(BUILDDIR)\cndump.exe $(SRCDIR)\cndump.c +$(BUILDDIR)\cncount.exe: $(SRCDIR)\cncount.c $(INCDIR)\platform.h $(INCDIR)\config.h + $(CC) $(CFLAGS) -e$(BUILDDIR)\cncount.exe $(SRCDIR)\cncount.c + +$(BUILDDIR)\cndel.exe: $(SRCDIR)\cndel.c $(INCDIR)\platform.h $(INCDIR)\config.h + $(CC) $(CFLAGS) -e$(BUILDDIR)\cndel.exe $(SRCDIR)\cndel.c + clean: if exist $(BUILDDIR)\*.exe del $(BUILDDIR)\*.exe if exist $(BUILDDIR)\*.obj del $(BUILDDIR)\*.obj diff --git a/Makefile b/Makefile index 9b335d5..955b9f6 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,10 @@ SRCDIR = src INCDIR = include BUILDDIR = build -SOURCES = $(SRCDIR)/cnadd.c $(SRCDIR)/cndump.c +SOURCES = $(SRCDIR)/cnadd.c $(SRCDIR)/cndump.c $(SRCDIR)/cncount.c $(SRCDIR)/cndel.c HEADERS = $(INCDIR)/platform.h $(INCDIR)/config.h -TARGETS = $(BUILDDIR)/cnadd $(BUILDDIR)/cndump +TARGETS = $(BUILDDIR)/cnadd $(BUILDDIR)/cndump $(BUILDDIR)/cncount $(BUILDDIR)/cndel .PHONY: all clean install uninstall @@ -27,6 +27,12 @@ $(BUILDDIR)/cnadd: $(SRCDIR)/cnadd.c $(HEADERS) $(BUILDDIR)/cndump: $(SRCDIR)/cndump.c $(HEADERS) $(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cndump.c +$(BUILDDIR)/cncount: $(SRCDIR)/cncount.c $(HEADERS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cncount.c + +$(BUILDDIR)/cndel: $(SRCDIR)/cndel.c $(HEADERS) + $(CC) $(CFLAGS) $(INCLUDES) -o $@ $(SRCDIR)/cndel.c + clean: rm -rf $(BUILDDIR) @@ -34,6 +40,8 @@ clean: install: $(TARGETS) install -m 755 $(BUILDDIR)/cnadd /usr/local/bin/ install -m 755 $(BUILDDIR)/cndump /usr/local/bin/ + install -m 755 $(BUILDDIR)/cncount /usr/local/bin/ + install -m 755 $(BUILDDIR)/cndel /usr/local/bin/ uninstall: - rm -f /usr/local/bin/cnadd /usr/local/bin/cndump + rm -f /usr/local/bin/cnadd /usr/local/bin/cndump /usr/local/bin/cncount /usr/local/bin/cndel diff --git a/README.md b/README.md new file mode 100644 index 0000000..4e9bd82 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# cnotes + +A simple command-line note-taking system written in strict C89/ANSI C for maximum portability. + +## Overview + +cnotes provides quick, one-line note capture from the terminal. Notes are stored in a simple CSV format and can be displayed, sorted, and filtered. + +## Supported Platforms + +- Linux/Unix (GCC) +- macOS (GCC/Clang) +- Windows (Turbo C++ 3.0, MinGW) +- DOS 6.22 (Turbo C++ 3.0) + +## Building + +### Linux/Unix/macOS + +```bash +make +``` + +Binaries are placed in the `build/` directory. + +### DOS/Windows (Turbo C++ 3.0) + +```batch +make -f MAKEFILE.TC +``` + +Or use the batch file if `make` is unavailable: + +```batch +BUILD.BAT +``` + +### Installation (Unix-like systems) + +```bash +sudo make install # installs to /usr/local/bin +sudo make uninstall # removes from /usr/local/bin +``` + +## Commands + +### cnadd + +Add a new timestamped note entry. + +```bash +cnadd "Remember to call Bob" +cnadd -c Work "Meeting at 3pm" +cnadd -c Personal "Buy groceries" +``` + +Options: +- `-c CATEGORY` - Specify category (max 10 chars, default: "General") + +### cndump + +Display all notes in a formatted table. + +```bash +cndump # display all notes +cndump -d # sort by date (newest first) +cndump -c # sort by category +cndump -r # reverse sort order +cndump -d -r # sort by date, oldest first +``` + +Options: +- `-d, --date` - Sort by date/time +- `-c, --category` - Sort by category +- `-r, --reverse` - Reverse sort order + +### cncount + +Display note statistics. + +```bash +cncount # total entry count +cncount -c # count by category +cncount -d # count by date +``` + +Options: +- `-c, --category` - Show counts per category +- `-d, --date` - Show counts per date + +### cndel + +Delete note entries. + +```bash +cndel -n 5 # delete entry at line 5 +cndel -l # delete the last (most recent) entry +cndel -d 2025-01-30 # delete all entries from a specific date +cndel -n 5 -y # delete without confirmation prompt +``` + +Options: +- `-n LINE_NUMBER` - Delete entry at specified line number +- `-d DATE` - Delete all entries matching DATE (YYYY-MM-DD) +- `-l` - Delete the last (most recent) entry +- `-y` - Skip confirmation prompt + +## Configuration + +### Notes Location + +By default, notes are stored in: + +| Platform | Location | +|----------|----------| +| Unix/Linux | `~/.local/share/cnotes/cnotes.csv` | +| Windows | `%USERPROFILE%\.cnotes\cnotes.csv` | +| DOS | Current directory or `%CNOTES_HOME%\cnotes.csv` | + +### Environment Variables + +- `CNOTES_PATH` - Override the notes directory location (highest priority) +- On DOS, set `CNOTES_HOME` if `HOME` is not available + +Example: +```bash +export CNOTES_PATH=/custom/path/to/notes +``` + +### Compile-time Configuration + +Override defaults by defining at compile time: + +```bash +gcc -DCNOTES_DIR=\".mynotes\" -DMAX_ENTRIES=1000 ... +``` + +## Data Format + +Notes are stored in CSV format: + +``` +DATE,TIME,CATEGORY ,"MESSAGE" +2025-01-30,14:30,Work ,"Meeting with team" +2025-01-30,09:15,Personal ,"Buy groceries" +``` + +Fields: +- Date: `YYYY-MM-DD` (10 chars) +- Time: `HH:MM` (5 chars) +- Category: padded to 10 chars +- Message: quoted, max 125 chars + +## TODO + +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 + cntail -n 10 # last 10 entries + ``` + +- [ ] **cncat** - List unique categories + ``` + cncat # list categories + cncat -c # with counts + ``` + +- [ ] **cnexport** - Export to other formats + ``` + cnexport > notes.txt # plain text + cnexport -t > notes.tsv # tab-separated + ``` + +## License + +[Your license here] + +## Author + +Gregory Gauthier diff --git a/compile_commands.json b/compile_commands.json new file mode 100644 index 0000000..9265138 --- /dev/null +++ b/compile_commands.json @@ -0,0 +1,22 @@ +[ + { + "directory": ".", + "command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cnadd.c -o build/cnadd.o", + "file": "src/cnadd.c" + }, + { + "directory": ".", + "command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cndump.c -o build/cndump.o", + "file": "src/cndump.c" + }, + { + "directory": ".", + "command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cncount.c -o build/cncount.o", + "file": "src/cncount.c" + }, + { + "directory": ".", + "command": "gcc -ansi -Wpedantic -Wall -Wextra -O2 -I include -c src/cndel.c -o build/cndel.o", + "file": "src/cndel.c" + } +] diff --git a/src/cncount.c b/src/cncount.c new file mode 100644 index 0000000..1ecf1e5 --- /dev/null +++ b/src/cncount.c @@ -0,0 +1,266 @@ +/* + * cncount - Display note statistics + */ + +#include +#include +#include +#include "platform.h" +#include "config.h" + +#define MAX_LENGTH 500 +#define DATE_LENGTH 10 +#define TIME_LENGTH 5 +#define MAX_UNIQUE 500 /* Max unique categories or dates */ + +typedef enum { + COUNT_TOTAL, + COUNT_BY_CATEGORY, + COUNT_BY_DATE +} CountMode; + +typedef struct { + char key[CATEGORY_LENGTH + 1]; + int count; +} CountEntry; + +/* Parse date field from CSV line */ +static int parse_date(const char *line, char *date) { + if (strlen(line) < DATE_LENGTH) return 0; + strncpy(date, line, DATE_LENGTH); + date[DATE_LENGTH] = '\0'; + return 1; +} + +/* Parse category field from CSV line */ +static int parse_category(const char *line, char *category) { + const char *ptr; + int i; + + /* Skip date and time fields: DATE,TIME,CATEGORY */ + if (strlen(line) < DATE_LENGTH + 1 + TIME_LENGTH + 1) return 0; + ptr = line + DATE_LENGTH + 1 + TIME_LENGTH + 1; + + /* Copy category until comma */ + i = 0; + while (*ptr != ',' && *ptr != '\0' && i < CATEGORY_LENGTH) { + category[i++] = *ptr++; + } + category[i] = '\0'; + + /* Trim trailing spaces */ + while (i > 0 && category[i - 1] == ' ') { + category[--i] = '\0'; + } + + return 1; +} + +/* Find or add a key in the counts array */ +static int find_or_add(CountEntry *counts, int *num_entries, const char *key) { + int i; + + /* Search for existing key */ + for (i = 0; i < *num_entries; i++) { + if (strcmp(counts[i].key, key) == 0) { + return i; + } + } + + /* Add new key if room */ + if (*num_entries >= MAX_UNIQUE) { + return -1; + } + + strncpy(counts[*num_entries].key, key, CATEGORY_LENGTH); + counts[*num_entries].key[CATEGORY_LENGTH] = '\0'; + counts[*num_entries].count = 0; + (*num_entries)++; + + return *num_entries - 1; +} + +/* Comparison function for sorting by key */ +static int compare_by_key(const void *a, const void *b) { + const CountEntry *ea = (const CountEntry *)a; + const CountEntry *eb = (const CountEntry *)b; + return strcmp(ea->key, eb->key); +} + +/* Comparison function for sorting by count (descending) */ +static int compare_by_count(const void *a, const void *b) { + const CountEntry *ea = (const CountEntry *)a; + const CountEntry *eb = (const CountEntry *)b; + return eb->count - ea->count; +} + +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_usage(const char *prog_name) { + fprintf(stderr, "Usage: %s [-c|--category] [-d|--date]\n", prog_name); + fprintf(stderr, " -c, --category Count entries by category\n"); + fprintf(stderr, " -d, --date Count entries by date\n"); + fprintf(stderr, " (no options) Show total count only\n"); +} + +int main(int argc, char *argv[]) { + int i; + FILE *notesfile; + char file_path[600]; + char line[MAX_LENGTH + 1]; + char key[CATEGORY_LENGTH + 1]; + int total_count = 0; + CountMode mode = COUNT_TOTAL; + CountEntry *counts; + int num_entries = 0; + int idx; + + /* Parse command line arguments */ + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--category") == 0) { + if (mode != COUNT_TOTAL) { + fprintf(stderr, "cncount: cannot specify multiple count modes\n"); + print_usage(argv[0]); + return 1; + } + mode = COUNT_BY_CATEGORY; + } + else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--date") == 0) { + if (mode != COUNT_TOTAL) { + fprintf(stderr, "cncount: cannot specify multiple count modes\n"); + print_usage(argv[0]); + return 1; + } + mode = COUNT_BY_DATE; + } + else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + else { + fprintf(stderr, "cncount: unknown option '%s'\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + /* Allocate counts array if needed */ + counts = NULL; + if (mode != COUNT_TOTAL) { + counts = (CountEntry *)malloc(MAX_UNIQUE * sizeof(CountEntry)); + if (counts == NULL) { + fprintf(stderr, "Error: Cannot allocate memory\n"); + return 1; + } + } + + /* Get cnotes file path */ + if (!get_cnotes_path(file_path, sizeof(file_path))) { + if (counts) free(counts); + 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"); + if (counts) free(counts); + return 1; + } + + /* Read and count entries */ + while (fgets(line, MAX_LENGTH, notesfile) != NULL) { + /* Skip empty lines */ + if (strlen(line) < DATE_LENGTH) continue; + + total_count++; + + if (mode == COUNT_BY_DATE) { + if (parse_date(line, key)) { + idx = find_or_add(counts, &num_entries, key); + if (idx >= 0) { + counts[idx].count++; + } + } + } + else if (mode == COUNT_BY_CATEGORY) { + if (parse_category(line, key)) { + idx = find_or_add(counts, &num_entries, key); + if (idx >= 0) { + counts[idx].count++; + } + } + } + } + fclose(notesfile); + + /* Print results */ + if (mode == COUNT_TOTAL) { + printf("Total entries: %d\n", total_count); + } + else { + /* Sort results */ + if (mode == COUNT_BY_DATE) { + qsort(counts, num_entries, sizeof(CountEntry), compare_by_key); + } else { + qsort(counts, num_entries, sizeof(CountEntry), compare_by_count); + } + + /* Print header */ + if (mode == COUNT_BY_DATE) { + printf("%-12s %s\n", "Date", "Count"); + printf("%-12s %s\n", "----------", "-----"); + } else { + printf("%-12s %s\n", "Category", "Count"); + printf("%-12s %s\n", "----------", "-----"); + } + + /* Print counts */ + for (i = 0; i < num_entries; i++) { + printf("%-12s %d\n", counts[i].key, counts[i].count); + } + + printf("%-12s %s\n", "----------", "-----"); + printf("%-12s %d\n", "Total", total_count); + } + + if (counts) free(counts); + return 0; +} diff --git a/src/cndel.c b/src/cndel.c new file mode 100644 index 0000000..241d6fb --- /dev/null +++ b/src/cndel.c @@ -0,0 +1,340 @@ +/* + * cndel - Delete note entries + */ + +#include +#include +#include +#include "platform.h" +#include "config.h" + +#define MAX_LENGTH 500 +#define DATE_LENGTH 10 +#define TIME_LENGTH 5 + +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; +} + +/* Get temporary file path */ +int get_temp_path(char *buffer, size_t bufsize, const char *original_path) { + size_t len = strlen(original_path); + if (len + 5 > bufsize) { + fprintf(stderr, "Error: Path too long\n"); + return 0; + } + sprintf(buffer, "%s.tmp", original_path); + return 1; +} + +/* Parse and display a line for confirmation */ +void display_entry(int line_num, const char *line) { + char date[DATE_LENGTH + 1]; + char time_str[TIME_LENGTH + 1]; + char category[CATEGORY_LENGTH + 1]; + const char *ptr; + const char *msg_start; + const char *msg_end; + int i; + + /* Parse date */ + if (strlen(line) < DATE_LENGTH) return; + strncpy(date, line, DATE_LENGTH); + date[DATE_LENGTH] = '\0'; + + /* Parse time */ + ptr = line + DATE_LENGTH + 1; + if (strlen(ptr) < TIME_LENGTH) return; + strncpy(time_str, ptr, TIME_LENGTH); + time_str[TIME_LENGTH] = '\0'; + + /* Parse category */ + ptr = ptr + TIME_LENGTH + 1; + i = 0; + while (*ptr != ',' && *ptr != '\0' && i < CATEGORY_LENGTH) { + category[i++] = *ptr++; + } + category[i] = '\0'; + /* Trim trailing spaces */ + while (i > 0 && category[i - 1] == ' ') { + category[--i] = '\0'; + } + + /* Find message (quoted) */ + msg_start = strchr(ptr, '"'); + if (msg_start) { + msg_start++; + msg_end = strchr(msg_start, '"'); + if (msg_end) { + printf(" Line %d: [%s %s] [%s] ", line_num, date, time_str, category); + /* Print message without trailing quote */ + while (msg_start < msg_end) { + putchar(*msg_start++); + } + putchar('\n'); + return; + } + } + + /* Fallback: just show the raw line */ + printf(" Line %d: %s", line_num, line); +} + +void print_usage(const char *prog_name) { + fprintf(stderr, "Usage: %s -n LINE_NUMBER [-y]\n", prog_name); + fprintf(stderr, " %s -d DATE [-y]\n", prog_name); + fprintf(stderr, " %s -l [-y]\n", prog_name); + fprintf(stderr, "\n"); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " -n LINE_NUMBER Delete entry at specified line number\n"); + fprintf(stderr, " -d DATE Delete all entries matching DATE (YYYY-MM-DD)\n"); + fprintf(stderr, " -l Delete the last (most recent) entry\n"); + fprintf(stderr, " -y Skip confirmation prompt\n"); +} + +int main(int argc, char *argv[]) { + int i; + FILE *infile; + FILE *outfile; + char file_path[600]; + char temp_path[610]; + char line[MAX_LENGTH + 1]; + int line_num; + int target_line = -1; + char *target_date = NULL; + int delete_last = 0; + int skip_confirm = 0; + int total_lines = 0; + int deleted_count = 0; + int confirm; + char response[10]; + + /* Parse command line arguments */ + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "-n") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "cndel: -n requires a line number\n"); + print_usage(argv[0]); + return 1; + } + target_line = atoi(argv[++i]); + if (target_line < 1) { + fprintf(stderr, "cndel: invalid line number '%s'\n", argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "-d") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "cndel: -d requires a date (YYYY-MM-DD)\n"); + print_usage(argv[0]); + return 1; + } + target_date = argv[++i]; + if (strlen(target_date) != DATE_LENGTH) { + fprintf(stderr, "cndel: invalid date format '%s' (use YYYY-MM-DD)\n", target_date); + return 1; + } + } + else if (strcmp(argv[i], "-l") == 0) { + delete_last = 1; + } + else if (strcmp(argv[i], "-y") == 0) { + skip_confirm = 1; + } + else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) { + print_usage(argv[0]); + return 0; + } + else { + fprintf(stderr, "cndel: unknown option '%s'\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + /* Validate options */ + if (target_line < 0 && target_date == NULL && !delete_last) { + fprintf(stderr, "cndel: must specify -n LINE_NUMBER, -d DATE, or -l\n"); + print_usage(argv[0]); + return 1; + } + + if ((target_line > 0) + (target_date != NULL) + delete_last > 1) { + fprintf(stderr, "cndel: cannot combine -n, -d, and -l options\n"); + print_usage(argv[0]); + return 1; + } + + /* Get file paths */ + if (!get_cnotes_path(file_path, sizeof(file_path))) { + return 1; + } + + if (!get_temp_path(temp_path, sizeof(temp_path), file_path)) { + return 1; + } + + /* If deleting last entry, first count total lines */ + if (delete_last) { + infile = fopen(file_path, "r"); + if (infile == NULL) { + fprintf(stderr, "Error: Cannot open file '%s'\n", file_path); + return 1; + } + while (fgets(line, MAX_LENGTH, infile) != NULL) { + if (strlen(line) >= DATE_LENGTH) { + total_lines++; + } + } + fclose(infile); + + if (total_lines == 0) { + fprintf(stderr, "cndel: no entries to delete\n"); + return 1; + } + target_line = total_lines; + } + + /* First pass: show what will be deleted and count matches */ + infile = fopen(file_path, "r"); + if (infile == NULL) { + fprintf(stderr, "Error: Cannot open file '%s'\n", file_path); + return 1; + } + + printf("The following entries will be deleted:\n\n"); + line_num = 0; + while (fgets(line, MAX_LENGTH, infile) != NULL) { + if (strlen(line) < DATE_LENGTH) continue; + line_num++; + + if (target_line > 0 && line_num == target_line) { + display_entry(line_num, line); + deleted_count++; + } + else if (target_date != NULL && strncmp(line, target_date, DATE_LENGTH) == 0) { + display_entry(line_num, line); + deleted_count++; + } + } + fclose(infile); + + if (deleted_count == 0) { + if (target_line > 0) { + fprintf(stderr, "cndel: line %d does not exist (file has %d entries)\n", + target_line, line_num); + } else { + fprintf(stderr, "cndel: no entries found matching date '%s'\n", target_date); + } + return 1; + } + + printf("\n%d entry(s) will be deleted.\n", deleted_count); + + /* Confirm deletion */ + if (!skip_confirm) { + printf("Are you sure? [y/N]: "); + fflush(stdout); + + if (fgets(response, sizeof(response), stdin) == NULL) { + printf("Aborted.\n"); + return 1; + } + + confirm = (response[0] == 'y' || response[0] == 'Y'); + if (!confirm) { + printf("Aborted.\n"); + return 0; + } + } + + /* Second pass: copy all lines except deleted ones to temp file */ + infile = fopen(file_path, "r"); + if (infile == NULL) { + fprintf(stderr, "Error: Cannot open file '%s'\n", file_path); + return 1; + } + + outfile = fopen(temp_path, "w"); + if (outfile == NULL) { + fprintf(stderr, "Error: Cannot create temporary file '%s'\n", temp_path); + fclose(infile); + return 1; + } + + line_num = 0; + deleted_count = 0; + while (fgets(line, MAX_LENGTH, infile) != NULL) { + if (strlen(line) < DATE_LENGTH) { + /* Preserve empty/invalid lines as-is */ + fputs(line, outfile); + continue; + } + line_num++; + + if (target_line > 0 && line_num == target_line) { + deleted_count++; + continue; /* Skip this line */ + } + else if (target_date != NULL && strncmp(line, target_date, DATE_LENGTH) == 0) { + deleted_count++; + continue; /* Skip this line */ + } + + /* Keep this line */ + fputs(line, outfile); + } + + fclose(infile); + fclose(outfile); + + /* Replace original with temp file */ + if (remove(file_path) != 0) { + fprintf(stderr, "Error: Cannot remove original file\n"); + remove(temp_path); + return 1; + } + + if (rename(temp_path, file_path) != 0) { + fprintf(stderr, "Error: Cannot rename temporary file\n"); + return 1; + } + + printf("Deleted %d entry(s).\n", deleted_count); + return 0; +}