add cncount and cndel commands
This commit is contained in:
parent
1c60165908
commit
b68b09b777
@ -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.
|
||||
|
||||
26
CMakeLists.txt
Normal file
26
CMakeLists.txt
Normal file
@ -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)
|
||||
@ -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
|
||||
|
||||
14
Makefile
14
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
|
||||
|
||||
189
README.md
Normal file
189
README.md
Normal file
@ -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
|
||||
22
compile_commands.json
Normal file
22
compile_commands.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
266
src/cncount.c
Normal file
266
src/cncount.c
Normal file
@ -0,0 +1,266 @@
|
||||
/*
|
||||
* cncount - Display note statistics
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
340
src/cndel.c
Normal file
340
src/cndel.c
Normal file
@ -0,0 +1,340 @@
|
||||
/*
|
||||
* cndel - Delete note entries
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#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;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user