agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit e807aad93c181a99b256dd97212f74704357d59c
parent 5038735bcd45a07b560b204e2e36b34ac93b5cfc
Author: Marc Coquand <marc@coquand.email>
Date:   Sat, 31 May 2025 20:03:07 +0100

Switch to hashmap implementation

Diffstat:
MMakefile | 10+++++-----
Ahashmap.c | 462+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahashmap.h | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mmain.c | 1100+++++++++++++++++++++++++++++++++++--------------------------------------------
4 files changed, 1024 insertions(+), 614 deletions(-)
diff --git a/Makefile b/Makefile
@@ -4,11 +4,11 @@ LIBS = `pkg-config fuse3 --cflags --libs`
 
 all: journalfs
 
-journalfs: main.c
-	$(CC) $(CFLAGS) main.c -o journalfs $(LIBS)
+journalfs: main.c hashmap.c
+	$(CC) $(CFLAGS) main.c hashmap.c -o journalfs $(LIBS)
 
-debug: main.c
-	$(CC) $(CFLAGS) $(DEBUG_FLAGS) main.c -o journalfs-debug $(LIBS)
+debug: main.c hashmap.c
+	$(CC) $(CFLAGS) $(DEBUG_FLAGS) main.c hashmap.c -o journalfs-debug $(LIBS)
 
 clean:
-	rm -f journalfs
+	rm -f journalfs journalfs-debug
diff --git a/hashmap.c b/hashmap.c
@@ -0,0 +1,462 @@
+// Taken from https://github.com/prismz/hashmap
+#include "hashmap.h"
+
+#include <inttypes.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef HM_DEBUG
+#include <stdarg.h>
+static void debug_print(char *fmt, ...)
+{
+	va_list ap;
+	va_start(ap, fmt);
+	vprintf(fmt, ap);
+	va_end(ap);
+}
+#else
+/* compiler should just optimize this away */
+static void debug_print() {}
+#endif
+
+static int hm_check_mem(void *ptr)
+{
+	if (ptr != NULL)
+		return 0;
+
+	if (HM_EXIT_ON_ALLOC_FAIL) {
+		fprintf(stderr, "failed to allocate memory\n");
+		exit(1);
+	}
+
+	return 1;
+}
+
+/* hash a single byte */
+static uint32_t fnv1a(unsigned char byte, uint32_t hash)
+{
+	return (byte ^ hash) * HM_FNV_PRIME;
+}
+
+static uint32_t fnv1a_hash(const char *str)
+{
+	uint32_t hash = HM_FNV_SEED;
+	while (*str)
+		hash = fnv1a((unsigned char)*str++, hash);
+	return hash;
+}
+
+/* will duplicate key */
+static struct bucket *new_bucket(const char *key, void *val)
+{
+	if (key == NULL)
+		return NULL;
+
+	struct bucket *b = HM_CALLOC_FUNC(1, sizeof(struct bucket));
+	if (hm_check_mem(b))
+		return NULL;
+
+	b->key = HM_STRDUP_FUNC(key);
+	if (hm_check_mem(b->key)) {
+		free(b);
+		return NULL;
+	}
+
+	b->val = val;
+	b->next = NULL;
+
+	b->hash = fnv1a_hash(key);
+
+	debug_print("creating bucket with key %s, hash of this key is %" PRIu32
+		    "\n",
+		    key, b->hash);
+
+	return b;
+}
+
+struct hashmap *hashmap_new(void (*val_free_func)(void *))
+{
+	/* Any smaller and we'd pretty much instantly have to resize */
+	size_t capacity = HM_DEFAULT_HASHMAP_SIZE;
+
+	struct hashmap *hm = HM_CALLOC_FUNC(1, sizeof(struct hashmap));
+	if (hm_check_mem(hm))
+		return NULL;
+
+	hm->capacity = capacity;
+	hm->n_buckets = 0;
+	hm->val_free_func = val_free_func;
+	hm->buckets = HM_CALLOC_FUNC(capacity, sizeof(struct bucket *));
+	if (hm_check_mem(hm->buckets)) {
+		free(hm);
+		return NULL;
+	}
+
+	return hm;
+}
+
+static void free_bucket(struct bucket *b, void (*val_free_func)(void *))
+{
+	if (b == NULL)
+		return;
+
+	struct bucket *curr;
+	while (b != NULL) {
+		curr = b;
+		b = b->next;
+
+		free(curr->key);
+		if (val_free_func != NULL && curr->val != NULL)
+			val_free_func(curr->val);
+		free(curr);
+	}
+}
+
+void hashmap_free(struct hashmap *hm)
+{
+	if (hm == NULL)
+		return;
+	for (size_t i = 0; i < hm->capacity; i++) {
+		if (hm->buckets[i] == NULL)
+			continue;
+		free_bucket(hm->buckets[i], hm->val_free_func);
+	}
+	free(hm->buckets);
+	free(hm);
+}
+
+int hashmap_resize(struct hashmap *hm)
+{
+	if (hm == NULL)
+		return -1;
+
+	size_t new_size = hm->capacity * HM_RESIZE_SCALE_FACTOR;
+
+	struct bucket **buckets =
+	    HM_CALLOC_FUNC(new_size, sizeof(struct bucket *));
+	if (hm_check_mem(buckets))
+		return 1;
+
+	for (size_t i = 0; i < hm->capacity; i++) {
+		if (hm->buckets[i] == NULL)
+			continue;
+
+		struct bucket *target = hm->buckets[i];
+		struct bucket *curr;
+		while (target != NULL) {
+			curr = target;
+			target = target->next;
+
+			size_t curr_new_idx = curr->hash % (uint32_t)(new_size);
+
+			if (buckets[curr_new_idx] == NULL) {
+				buckets[curr_new_idx] = curr;
+				buckets[curr_new_idx]->next = NULL;
+				continue;
+			}
+
+			/* collision */
+
+			struct bucket *ptr = buckets[curr_new_idx];
+			while (ptr->next != NULL)
+				ptr = ptr->next;
+
+			ptr->next = curr;
+			ptr->next->next = NULL;
+		}
+
+		/* size_t new_idx = hm->buckets[i]->hash % (uint32_t)new_size;
+		debug_print("buckets at idx %ld move to idx %ld\n", i, new_idx);
+		if (buckets[new_idx] != NULL) {
+			free(buckets);
+			return 1;
+		}
+		buckets[new_idx] = hm->buckets[i]; */
+	}
+
+	free(hm->buckets);
+	hm->buckets = buckets;
+	hm->capacity = new_size;
+
+	return 0;
+}
+
+int hashmap_insert(struct hashmap *hm, const char *key, void *val)
+{
+	if (hm == NULL || key == NULL)
+		return 1;
+
+	if ((float)(hm->n_buckets + 1) >
+	    (HM_HASHMAP_MAX_LOAD * (float)hm->capacity)) {
+		if (hashmap_resize(hm))
+			return -1;
+	}
+
+	hm->n_buckets++;
+	struct bucket *b = new_bucket(key, val);
+	if (hm_check_mem(b))
+		return 1;
+	size_t idx = (b->hash) % (uint32_t)(hm->capacity);
+
+	struct bucket *target_bucket = hm->buckets[idx];
+	if (target_bucket == NULL) {
+		debug_print("insert: bucket with key %s goes to idx %ld\n",
+			    b->key, idx);
+		hm->buckets[idx] = b;
+		return 0;
+	}
+
+	/* changing value of something already in the hashmap */
+	struct bucket *curr;
+	while (target_bucket != NULL) {
+		curr = target_bucket;
+		target_bucket = target_bucket->next;
+		if (curr->hash == b->hash && strcmp(curr->key, key) == 0) {
+			debug_print("key already exists, reassigning value\n");
+
+			/* we free the old value, should probably add an
+			 * option for this */
+			if (hm->val_free_func != NULL)
+				hm->val_free_func(curr->val);
+			curr->val = val;
+			free_bucket(b, NULL);
+			return 0;
+		}
+	}
+
+	debug_print("collision. appending to linked list\n");
+
+	target_bucket = hm->buckets[idx];
+
+	while (target_bucket->next != NULL)
+		target_bucket = target_bucket->next;
+
+	target_bucket->next = b;
+	b->next = NULL;
+
+	return 0;
+}
+
+void *hashmap_get(struct hashmap *hm, const char *key)
+{
+	if (hm == NULL || key == NULL)
+		return NULL;
+
+	uint32_t hash = fnv1a_hash(key);
+	size_t idx = hash % (uint32_t)(hm->capacity);
+	debug_print("retreiving object with key %s - hashes to %" PRIu32
+		    " giving idx %ld\n",
+		    key, hash, idx);
+
+	struct bucket *target_bucket = hm->buckets[idx];
+	if (target_bucket == NULL)
+		return NULL;
+
+	if (target_bucket->hash == hash && strcmp(target_bucket->key, key) == 0)
+		return target_bucket->val;
+
+	/* collision */
+	struct bucket *curr;
+	while (target_bucket != NULL) {
+		curr = target_bucket;
+		target_bucket = target_bucket->next;
+		if (curr->hash == hash && strcmp(curr->key, key) == 0)
+			return curr->val;
+	}
+
+#ifdef HM_DONTTRUSTHASH
+	/* search all items as a last resort */
+	for (size_t i = 0; i < hm->capacity; i++) {
+		struct bucket *b = hm->buckets[i];
+		struct bucket *curr;
+		while (b != NULL) {
+			curr = b;
+			b = b->next;
+
+			if (curr->hash == hash && strcmp(curr->key, key) == 0)
+				return curr->val;
+		}
+	}
+#endif
+
+	return NULL;
+}
+
+/*
+ * wrapper function, so that if we don't find it with normal
+ * methods, we can easily re-call the function with a custom index, allowing
+ * us to iterate through the hashmap to find the item if for some
+ * reason the hash gives an incorrect index
+ */
+static int _hashmap_remove(struct hashmap *hm, const char *key, uint32_t hash,
+			   size_t idx)
+{
+	if (hm == NULL || key == NULL)
+		return 1;
+
+	// uint32_t hash = fnv1a_hash(key);
+	// size_t idx = hash % (uint32_t)(hm->capacity);
+
+	debug_print("removing object with key %s which hashes to %" PRIu32
+		    ". Supplied index to search is %ld\n",
+		    key, hash, idx);
+
+	struct bucket *target = hm->buckets[idx];
+	if (target == NULL)
+		return 1;
+
+	if (target->hash == hash && strcmp(target->key, key) == 0) {
+		/* bucket with single item */
+		if (target->next == NULL) {
+			debug_print(
+			    "simple removal of bucket with single node\n");
+			free_bucket(hm->buckets[idx], hm->val_free_func);
+			hm->buckets[idx] = NULL;
+			hm->n_buckets--;
+			return 0;
+		}
+
+		/* bucket where first item is to be removed,
+		 * but there are other items in the linked list */
+		debug_print("removing first node and preserving the rest of "
+			    "the linked list\n");
+
+		struct bucket *next = hm->buckets[idx]->next;
+		hm->buckets[idx] = next;
+		target->next = NULL;
+		free_bucket(target, hm->val_free_func);
+		return 0;
+	}
+
+	/* the bucket to be removed is not the first
+	 * bucket in the linked list */
+	debug_print(
+	    "bucket to be removed is within the linked list - searching...\n");
+	if (target->next == NULL)
+		return 1;
+
+	target = target->next;
+
+	struct bucket *curr = target;
+	struct bucket *prev = NULL;
+
+	while (target != NULL) {
+		curr = target;
+		target = target->next;
+
+		if (curr->hash == hash && strcmp(curr->key, key) != 0) {
+			prev = curr;
+			continue;
+		}
+
+		if (prev == NULL) {
+			debug_print("this should never happen...\n");
+			return 1;
+		}
+
+		prev->next = curr->next;
+		curr->next = NULL;
+		free_bucket(curr, hm->val_free_func);
+
+		return 0;
+	}
+
+	return 1;
+}
+
+int hashmap_remove(struct hashmap *hm, const char *key)
+{
+	if (hm == NULL || key == NULL)
+		return 1;
+
+	uint32_t hash = fnv1a_hash(key);
+	size_t idx = hash % (uint32_t)(hm->capacity);
+
+	if (_hashmap_remove(hm, key, hash, idx) == 0)
+		return 0;
+
+#ifdef HM_DONTTRUSTHASH
+	debug_print("couldn't find item to remove by hashing, must search "
+		    "entire hashmap\n");
+	for (size_t i = 0; i < hm->capacity; i++) {
+		/* skip if we already processed the index or if the item is NULL
+		 */
+		if (hm->buckets[i] == NULL || i == idx)
+			continue;
+
+		if (_hashmap_remove(hm, key, hash, i) == 0) {
+			debug_print(
+			    "successfully found and removed item at idx %ld\n",
+			    i);
+			return 0;
+		}
+	}
+
+	debug_print("couldn't find item to remove anywhere, returning 1\n");
+#endif
+
+	return 1;
+}
+
+/* only works for hashmap with strings as values */
+void hashmap_print(struct hashmap *hm)
+{
+	if (hm == NULL) {
+		printf("NULL\n");
+		return;
+	}
+
+	printf("Printing hashmap containing %ld buckets with capacity %ld:\n",
+	       hm->n_buckets, hm->capacity);
+
+	for (size_t i = 0; i < hm->capacity; i++) {
+		struct bucket *target = hm->buckets[i];
+		printf("bucket %02ld:\n", i);
+		if (target == NULL)
+			continue;
+
+		struct bucket *curr;
+		int n = 0;
+		while (target != NULL) {
+			curr = target;
+			target = target->next;
+			printf("    %02d: %s=%s\n", n, curr->key,
+			       (char *)curr->val);
+
+			n++;
+		}
+	}
+}
+
+char **hashmap_get_keys(struct hashmap *hm, size_t *n_keys)
+{
+	if (!hm || !n_keys)
+		return NULL;
+
+	char **keys = malloc(hm->n_buckets * sizeof(char *));
+	if (!keys)
+		return NULL;
+
+	size_t count = 0;
+	for (size_t i = 0; i < hm->capacity; i++) {
+		struct bucket *b = hm->buckets[i];
+		while (b) {
+			keys[count++] = strdup(b->key);
+			b = b->next;
+		}
+	}
+
+	*n_keys = count;
+	return keys;
+}
+
+void hashmap_free_keys(char **keys, size_t n_keys)
+{
+	if (!keys)
+		return;
+	for (size_t i = 0; i < n_keys; i++)
+		free(keys[i]);
+	free(keys);
+}
diff --git a/hashmap.h b/hashmap.h
@@ -0,0 +1,66 @@
+// Taken from https://github.com/prismz/hashmap
+//
+#ifndef HASHMAP_H
+#define HASHMAP_H
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define HM_CALLOC_FUNC calloc
+#define HM_STRDUP_FUNC strdup
+#define HM_EXIT_ON_ALLOC_FAIL 1
+#define HM_HASHMAP_MAX_LOAD 0.70f
+#define HM_RESIZE_SCALE_FACTOR 2
+#define HM_DEFAULT_HASHMAP_SIZE 16
+
+/* enabling this will print something for
+ * each internal action, not recommended */
+// #define HM_DEBUG
+
+/* if enabled this will not trust the hash function.
+ * It will search every item in the hashmap (after trying
+ * the hashing method). */
+// #define HM_DONTTRUSTHASH
+
+/*
+ * if using structs as values, this conveniently
+ * typecasts your struct's free function
+ * so that it can be passed to new_hashmap_item()
+ */
+#define hashmap_item_free_func(a) (void (*)(void *)) a
+
+/* https://create.stephan-brumme.com/fnv-hash */
+#define HM_FNV_PRIME 0x01000193 //   16777619
+#define HM_FNV_SEED 0x811C9DC5	// 2166136261
+typedef void (*hashmap_iter_callback)(const char *key, void *value,
+				      void *user_data);
+
+struct bucket;
+struct bucket {
+	uint32_t hash;
+	char *key;
+	void *val;
+
+	/* collisions are stored as linked lists */
+	struct bucket *next;
+};
+
+struct hashmap {
+	struct bucket **buckets;
+	size_t n_buckets;
+	size_t capacity;
+	void (*val_free_func)(void *);
+};
+
+struct hashmap *hashmap_new(void (*val_free_func)(void *));
+void hashmap_free(struct hashmap *hm);
+int hashmap_resize(struct hashmap *hm);
+int hashmap_insert(struct hashmap *hm, const char *key, void *val);
+void *hashmap_get(struct hashmap *hm, const char *key);
+int hashmap_remove(struct hashmap *hm, const char *key);
+void hashmap_print(struct hashmap *hm);
+char **hashmap_get_keys(struct hashmap *hm, size_t *n_keys);
+void hashmap_free_keys(char **keys, size_t n_keys);
+#endif /* HASHMAP_H */
diff --git a/main.c b/main.c
@@ -1,19 +1,23 @@
 #define FUSE_USE_VERSION 31
 
-#include <fuse3/fuse.h>
-#include <stdio.h>
-#include <string.h>
-#include <stdlib.h>
+#include "hashmap.h"
 #include <dirent.h>
 #include <errno.h>
-#include <time.h>
-#include <sys/stat.h>
-#include <regex.h>
+#include <fuse3/fuse.h>
 #include <pthread.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
 #include <sys/inotify.h>
+#include <sys/stat.h>
+#include <time.h>
 #include <unistd.h>
 #define ICS_DIR "/home/mccd/dev/fuse-journal/sample_ics"
-#define LOG(fmt, ...) fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
+#define LOG(fmt, ...)                                                          \
+	fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
+
+#define MAX_BASE_FILENAME_LEN (sizeof(unique_filename) - 1 - 11)
 
 // Listening to file changes of the original content
 #define EVENT_BUF_LEN (1024 * (sizeof(struct inotify_event) + 16))
@@ -21,12 +25,12 @@
 const char *display_date_fmt = "%b %d, %Y at %H:%M";
 const char *date_title_fmt = "%Y%m%d-%H%M%S";
 
-typedef struct 
-{
+typedef struct {
 	char *filename;
 	char *filename_original;
 	char *summary;
 	char *description;
+	char *uid;
 	time_t timestamp;
 	time_t created_at;
 } journal_entry;
@@ -34,12 +38,14 @@ typedef struct
 static journal_entry *entries = NULL;
 static size_t entry_count = 0;
 
+struct hashmap *entries_map = NULL;
 static pthread_mutex_t entries_mutex = PTHREAD_MUTEX_INITIALIZER;
 
-static void free_entry(journal_entry *entry) 
+static void free_entry(journal_entry *entry)
 {
-	if (entry) 
-    	{
+	if (entry) {
+
+		free(entry->uid);
 		free(entry->filename);
 		free(entry->filename_original);
 		free(entry->summary);
@@ -47,10 +53,21 @@ static void free_entry(journal_entry *entry)
 	}
 }
 
-static void free_all_entries() 
+void free_journal_entry(void *val)
+{
+	journal_entry *entry = (journal_entry *)val;
+	if (!entry)
+		return;
+	free(entry->filename_original);
+	free(entry->filename);
+	free(entry->summary);
+	free(entry->description);
+	free(entry);
+}
+
+static void free_all_entries()
 {
-	for (size_t i = 0; i < entry_count; i++) 
-    	{
+	for (size_t i = 0; i < entry_count; i++) {
 		free_entry(&entries[i]);
 	}
 	free(entries);
@@ -58,52 +75,59 @@ static void free_all_entries()
 	entry_count = 0;
 }
 
-static time_t parse_datetime(const char *datetime) 
+static time_t parse_datetime(const char *datetime)
 {
-	struct tm tm = 
-	{0};
-	int ret = sscanf(datetime, "%4d%2d%2dT%2d%2d%2dZ",
-			&tm.tm_year, &tm.tm_mon, &tm.tm_mday,
-			&tm.tm_hour, &tm.tm_min, &tm.tm_sec);
-	if (ret == 6) 
-    	{
-		tm.tm_year -= 1900;  // tm_year is years since 1900
-		tm.tm_mon -= 1;	  // tm_mon is months since January (0-11)
-		tm.tm_isdst = -1;	// Let mktime determine if daylight savings is needed
+	struct tm tm = {0};
+	int ret =
+	    sscanf(datetime, "%4d%2d%2dT%2d%2d%2dZ", &tm.tm_year, &tm.tm_mon,
+		   &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec);
+	if (ret == 6) {
+		tm.tm_year -= 1900; // tm_year is years since 1900
+		tm.tm_mon -= 1;	    // tm_mon is months since January (0-11)
+		tm.tm_isdst =
+		    -1; // Let mktime determine if daylight savings is needed
 		return mktime(&tm);
 	}
 	return -1;
 }
 
-
-
-
-static int compile_regexes(regex_t *regex_summary, regex_t *regex_description, regex_t *regex_dtstamp, regex_t *regex_lastmod) 
+static int compile_regexes(regex_t *regex_summary, regex_t *regex_description,
+			   regex_t *regex_dtstamp, regex_t *regex_lastmod,
+			   regex_t *regex_uid)
 {
 	// Parse text after SUMMARY: up until \r\n
-	if (regcomp(regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0) return -1;
+	if (regcomp(regex_summary, "SUMMARY:([^\r\n]+)", REG_EXTENDED) != 0)
+		return -1;
 	// Parse text after DESCRIPTION:, multiple lines up until \r\n
-	if (regcomp(regex_description, "DESCRIPTION:([^\r\n]*)", REG_EXTENDED) != 0) return -1;
-	if (regcomp(regex_dtstamp, "DTSTAMP:([0-9T]+Z)", REG_EXTENDED) != 0) return -1;
-	if (regcomp(regex_lastmod, "LAST-MODIFIED:([0-9T]+Z)", REG_EXTENDED) != 0) return -1;
+	if (regcomp(regex_description, "DESCRIPTION:([^\r\n]*)",
+		    REG_EXTENDED) != 0)
+		return -1;
+	if (regcomp(regex_dtstamp, "DTSTAMP:([0-9T]+Z)", REG_EXTENDED) != 0)
+		return -1;
+	if (regcomp(regex_lastmod, "LAST-MODIFIED:([0-9T]+Z)", REG_EXTENDED) !=
+	    0)
+		return -1;
+	if (regcomp(regex_uid, "UID:([^\r\n]+)", REG_EXTENDED) != 0)
+		return -1;
 	return 0;
 }
 
 // Extracts the matched text from regexp
-static char *extract_match(const char *content, regmatch_t match) 
+static char *extract_match(const char *content, regmatch_t match)
 {
 	size_t len = match.rm_eo - match.rm_so;
 	char *str = calloc(len + 1, sizeof(char));
-	if (!str) return NULL;
+	if (!str)
+		return NULL;
 	memcpy(str, content + match.rm_so, len);
 	str[len] = '\0';
 	return str;
 }
 
 // RFC 5545 specifies how human-readable text is escaped
-// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11 
-static void unescape_text(char *desc) 
-{	
+// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
+static void unescape_text(char *desc)
+{
 	char *src = desc;
 	char *dst = desc;
 
@@ -111,85 +135,102 @@ static void unescape_text(char *desc)
 		if (*src == '\\') {
 			src++;
 			switch (*src) {
-				case 'n':
-				case 'N':
-					*dst++ = '\n';
-					break;
-				case '\\':
-					*dst++ = '\\';
-					break;
-				case ',':
-					*dst++ = ',';
-					break;
-				case ';':
-					*dst++ = ';';
-					break;
-				case '\0':
-					goto done;
-				default:
-					*dst++ = '\\';
-					*dst++ = *src;
-					break;
+			case 'n':
+			case 'N':
+				*dst++ = '\n';
+				break;
+			case '\\':
+				*dst++ = '\\';
+				break;
+			case ',':
+				*dst++ = ',';
+				break;
+			case ';':
+				*dst++ = ';';
+				break;
+			case '\0':
+				goto done;
+			default:
+				*dst++ = '\\';
+				*dst++ = *src;
+				break;
 			}
 			src++;
-		} else {
+		}
+		else {
 			*dst++ = *src++;
 		}
 	}
-	done:
-		*dst = '\0';
+done:
+	*dst = '\0';
 }
 
-
-
-
-
 // RFC 5545 specifies how human-readable text is escaped
-// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11 
-static char *escape_text(const char *src) 
+// https://datatracker.ietf.org/doc/html/rfc5545#section-3.3.11
+static char *escape_text(const char *src)
 {
-	if (!src) return NULL;
+	if (!src)
+		return NULL;
 
 	size_t len = strlen(src);
 	// Worst-case size (every char becomes \X): len * 2 + 1
 	char *escaped = calloc(len * 2 + 1, 1);
-	if (!escaped) return NULL;
+	if (!escaped)
+		return NULL;
 
 	char *dst = escaped;
-	for (const char *p = src; *p; ++p) 
-    {
-		switch (*p) 
-        {
-			case '\n':
-				*dst++ = '\\';
-				*dst++ = 'n';
-				break;
-			case '\\':
-				*dst++ = '\\';
-				*dst++ = '\\';
-				break;
-			case ',':
-				*dst++ = '\\';
-				*dst++ = ',';
-				break;
-			case ';':
-				*dst++ = '\\';
-				*dst++ = ';';
-				break;
-			default:
-				*dst++ = *p;
-				break;
+	for (const char *p = src; *p; ++p) {
+		switch (*p) {
+		case '\n':
+			*dst++ = '\\';
+			*dst++ = 'n';
+			break;
+		case '\\':
+			*dst++ = '\\';
+			*dst++ = '\\';
+			break;
+		case ',':
+			*dst++ = '\\';
+			*dst++ = ',';
+			break;
+		case ';':
+			*dst++ = '\\';
+			*dst++ = ';';
+			break;
+		default:
+			*dst++ = *p;
+			break;
 		}
 	}
 	*dst = '\0';
 	return escaped;
 }
 
-static int parse_ics_file(const char *file_path, journal_entry *entry) 
+static journal_entry *get_entry_from_path(const char *path)
+{
+	if (!path || path[0] != '/') {
+		LOG("get_entry_from_path: invalid path '%s'\n",
+		    path ? path : "NULL");
+		return NULL;
+	}
+
+	const char *key = path + 1; // Strip leading '/'
+	if (strlen(key) == 0) {
+		LOG("get_entry_from_path: empty key for path '%s'\n", path);
+		return NULL;
+	}
+
+	LOG("get_entry_from_path: looking up key '%s'\n", key);
+	journal_entry *entry = hashmap_get(entries_map, key);
+	if (!entry)
+		LOG("get_entry_from_path: no entry found for key '%s'\n", key);
+	return entry;
+}
+
+static int parse_ics_file(const char *file_path, journal_entry *entry)
 {
 	FILE *file = fopen(file_path, "r");
-	if (!file) 
-    	{
+	if (!file) {
 		perror("Failed to open .ics file");
 		return -1;
 	}
@@ -200,98 +241,105 @@ static int parse_ics_file(const char *file_path, journal_entry *entry)
 	fseek(file, 0, SEEK_SET);
 
 	char *file_content = calloc(file_size + 1, sizeof(char));
-	if (!file_content) 
-    	{
+	if (!file_content) {
 		perror("Memory allocation failed");
 		fclose(file);
 		return -1;
 	}
 	fread(file_content, 1, file_size, file);
-	file_content[file_size] = '\0';  // Null-terminate the string
+	file_content[file_size] = '\0'; // Null-terminate the string
 	fclose(file);
 
-	regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod;
-	regmatch_t matches_summary[2], matches_description[2], matches_dtstamp[2], matches_lastmod[2];
+	regex_t regex_summary, regex_description, regex_dtstamp, regex_lastmod,
+	    regex_uid;
+	regmatch_t matches_summary[2], matches_description[2],
+	    matches_dtstamp[2], matches_lastmod[2], matches_uid[2];
 
 	size_t return_code = -1;
 
-	if (compile_regexes(&regex_summary, &regex_description, &regex_dtstamp, &regex_lastmod) != 0) 
-    	{
+	if (compile_regexes(&regex_summary, &regex_description, &regex_dtstamp,
+			    &regex_lastmod, &regex_uid) != 0) {
 		fprintf(stderr, "Failed to compile regular expressions\n");
 		goto cleanup;
 	}
 
 	// SUMMARY
-	if (regexec(&regex_summary, file_content, 2, matches_summary, 0) != 0) 
-    	{
+	if (regexec(&regex_summary, file_content, 2, matches_summary, 0) != 0) {
 		fprintf(stderr, "SUMMARY field not found in: %s\n", file_path);
 		goto cleanup;
-	 } 
+	}
 
 	entry->summary = extract_match(file_content, matches_summary[1]);
-	if (!entry->summary) 
-    	{
+	if (!entry->summary) {
 		perror("Memory allocation failed for SUMMARY");
 		goto cleanup;
 	}
 	unescape_text(entry->summary);
 
+	// UID
+	if (regexec(&regex_uid, file_content, 2, matches_uid, 0) == 0) {
+		entry->uid = extract_match(file_content, matches_uid[1]);
+	}
+	else {
+		// If missing, generate one (fallback)
+		LOG("FAILED TO GET UID");
+		asprintf(&entry->uid, "%ld@fuse-journal", time(NULL));
+	}
+
 	// DESCRIPTION
-	if (regexec(&regex_description, file_content, 2, matches_description, 0) != 0) 
-    	{
-		fprintf(stderr, "DESCRIPTION field not found in: %s\n", file_path);
+	if (regexec(&regex_description, file_content, 2, matches_description,
+		    0) != 0) {
+		fprintf(stderr, "DESCRIPTION field not found in: %s\n",
+			file_path);
 		goto cleanup;
 	}
-	entry->description = extract_match(file_content, matches_description[1]);
-	if (!entry->description) 
-    	{
+	entry->description =
+	    extract_match(file_content, matches_description[1]);
+	if (!entry->description) {
 		perror("Memory allocation failed for DESCRIPTION");
 		goto cleanup;
 	}
 	unescape_text(entry->description);
 
-
 	// Extract the DTSTAMP, our created_at
-	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) 
-    	{
-		char *dtstamp_str = extract_match(file_content, matches_dtstamp[1]);
-		if (!dtstamp_str) 
-    		{
+	if (regexec(&regex_dtstamp, file_content, 2, matches_dtstamp, 0) == 0) {
+		char *dtstamp_str =
+		    extract_match(file_content, matches_dtstamp[1]);
+		if (!dtstamp_str) {
 			perror("Memory allocation failed for DTSTAMP");
 			goto cleanup;
 		}
 		entry->created_at = parse_datetime(dtstamp_str);
 		free(dtstamp_str);
-		if (entry->created_at == -1) 
-    		{
-			fprintf(stderr, "Failed to parse DTSTAMP in: %s\n", file_path);
+		if (entry->created_at == -1) {
+			fprintf(stderr, "Failed to parse DTSTAMP in: %s\n",
+				file_path);
 			goto cleanup;
 		}
-	} else 
-    	{
+	}
+	else {
 		entry->created_at = time(NULL);
 	}
 
-
-	// LAST-MODIFIED 
-	if (regexec(&regex_lastmod, file_content, 2, matches_lastmod, 0) == 0) 
-    	{
-		char *lastmod_str = extract_match(file_content, matches_lastmod[1]);
-		if (!lastmod_str) 
-    		{
+	// LAST-MODIFIED
+	if (regexec(&regex_lastmod, file_content, 2, matches_lastmod, 0) == 0) {
+		char *lastmod_str =
+		    extract_match(file_content, matches_lastmod[1]);
+		if (!lastmod_str) {
 			perror("Memory allocation failed for LAST-MODIFIED");
 			goto cleanup;
 		}
 		entry->timestamp = parse_datetime(lastmod_str);
 		free(lastmod_str);
-		if (entry->timestamp == -1) 
-    		{
-			fprintf(stderr, "Failed to parse LAST-MODIFIED in: %s\n", file_path);
+		if (entry->timestamp == -1) {
+			fprintf(stderr,
+				"Failed to parse LAST-MODIFIED in: %s\n",
+				file_path);
 			goto cleanup;
 		}
-	} else 
-    	{
-		entry->timestamp = entry->created_at;  
+	}
+	else {
+		entry->timestamp = entry->created_at;
 	}
 
 	// Success and goto cleanup
@@ -306,52 +354,92 @@ cleanup:
 	return return_code;
 }
 
-static void load_journal_entries() 
+static void load_journal_entries()
 {
 	pthread_mutex_lock(&entries_mutex);
 
-	free_all_entries();
+	if (entries_map) {
+		hashmap_free(entries_map);
+	}
+	entries_map = hashmap_new(free_journal_entry);
+
 	LOG("Loading journal entries from: %s\n", ICS_DIR);
 	DIR *dir = opendir(ICS_DIR);
-	if (!dir) 
-    	{
+	if (!dir) {
+		pthread_mutex_unlock(&entries_mutex);
 		perror("opendir");
 		return;
 	}
 
 	struct dirent *entry;
 	char filepath[512];
-	while ((entry = readdir(dir))) 
-    	{
-		if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) 
-    		{
-			snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR, entry->d_name);
-			journal_entry new_entry;
-			new_entry.filename_original = strdup(entry->d_name);
-			if (parse_ics_file(filepath, &new_entry) == 0) 
-    			{
-                                char filename_time_str[64];
-                                struct tm tm_info; 
-                                localtime_r(&new_entry.created_at, &tm_info);
-                                strftime(filename_time_str, sizeof(filename_time_str), date_title_fmt, &tm_info);
-                                new_entry.filename = strdup(filename_time_str); 
-
-                                entries = realloc(entries, sizeof(journal_entry) * (entry_count + 1));
-				entries[entry_count] = new_entry;
-				entry_count++;
-			} else 
-    			{
-				free(new_entry.filename);
+	while ((entry = readdir(dir))) {
+		if (entry->d_type == DT_REG && strstr(entry->d_name, ".ics")) {
+			snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR,
+				 entry->d_name);
+			journal_entry *new_entry =
+			    calloc(1, sizeof(journal_entry));
+			if (!new_entry) {
+				perror("calloc failed");
+				continue;
+			}
+			new_entry->filename_original = strdup(entry->d_name);
+			if (parse_ics_file(filepath, new_entry) == 0) {
+				LOG("Parsed entry");
+
+				char filename_time_str[64];
+				struct tm tm_info;
+				localtime_r(&new_entry->created_at, &tm_info);
+				strftime(filename_time_str,
+					 sizeof(filename_time_str),
+					 date_title_fmt, &tm_info);
+
+				// Check for duplicate and append _N if exists
+				char base_filename[128];
+				strncpy(base_filename, filename_time_str,
+					sizeof(base_filename));
+				base_filename[sizeof(base_filename) - 12] =
+				    '\0'; // make space for suffix + .txt
+
+				char unique_filename[140]; // 128 base + 12 for
+							   // _N.txt suffix max
+				snprintf(unique_filename,
+					 sizeof(unique_filename), "%s.txt",
+					 base_filename);
+
+				int suffix = 1;
+				// Check if filename already exists in hashmap
+				while (hashmap_get(entries_map,
+						   unique_filename) != NULL) {
+					snprintf(unique_filename,
+						 sizeof(unique_filename),
+						 "%s_%d.txt", base_filename,
+						 suffix++);
+				}
+
+				LOG("Inserted entry %s", unique_filename);
+
+				new_entry->filename = strdup(unique_filename);
+				if (hashmap_insert(entries_map, unique_filename,
+						   new_entry) != 0) {
+					LOG("Failed to insert entry %s into "
+					    "hashmap\n",
+					    unique_filename);
+					free_journal_entry(new_entry);
+				}
+			}
+			else {
+				free(new_entry->filename);
 			}
 		}
 	}
-	LOG("Loaded %zu journal entries.\n", entry_count);
 	closedir(dir);
 
 	pthread_mutex_unlock(&entries_mutex);
 }
 
-static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi) 
+int journal_getattr(const char *path, struct stat *stbuf,
+		    struct fuse_file_info *fi)
 {
 	pthread_mutex_lock(&entries_mutex);
 
@@ -359,49 +447,36 @@ static int journal_getattr(const char *path, struct stat *stbuf, struct fuse_fil
 
 	int ret_code = 0;
 
-	if (strcmp(path, "/") == 0) 
-    	{
+	if (strcmp(path, "/") == 0) {
 		stbuf->st_mode = S_IFDIR | 0755;
 		stbuf->st_nlink = 2;
-		ret_code = 0;
 		goto unlock_and_return;
 	}
 
-	for (size_t i = 0; i < entry_count; i++) 
-    	{
-		char entry_path[256];
-		snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-		if (strcmp(path, entry_path) == 0) 
-    		{
-        		// S_IFREG = Regular file
-			stbuf->st_mode = S_IFREG | 0444;
-			// Regular file has one link (name in directory)
-			stbuf->st_nlink = 1;
-                        struct tm tm_info; 
-                        localtime_r(&entries[i].timestamp, &tm_info);
-			char timestamp_str[64];
-			strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt, &tm_info);
-
-			// Calculate the file size as in journal_read
-			int size = snprintf(NULL, 0, "%s\n%s\n---\n%s\n",
-					entries[i].summary, timestamp_str, entries[i].description);
+	journal_entry *entry = get_entry_from_path(path);
+	if (entry) {
+		stbuf->st_mode = S_IFREG | 0444;
+		stbuf->st_nlink = 1;
 
-			stbuf->st_size = (off_t)size;
+		struct tm tm_info;
+		localtime_r(&entry->timestamp, &tm_info);
+		char timestamp_str[64];
+		strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt,
+			 &tm_info);
 
-                        // Use the timestamp as the modification time
-                        stbuf->st_mtime = entries[i].timestamp;  
+		int size =
+		    snprintf(NULL, 0, "%s\n%s\n---\n%s\n", entry->summary,
+			     timestamp_str, entry->description);
+		stbuf->st_size = (off_t)size;
 
-                        // Set the change time to the same as the modification time
-                        stbuf->st_ctime = entries[i].timestamp;  
+		stbuf->st_mtime = entry->timestamp;
+		stbuf->st_ctime = entry->timestamp;
+		stbuf->st_atime = time(NULL);
 
-			// Access time is current time
-			stbuf->st_atime = time(NULL);			 
-
-
-			ret_code = 0;
-			goto unlock_and_return;
-		}
+		ret_code = 0;
+		goto unlock_and_return;
 	}
+
 	ret_code = -ENOENT;
 
 unlock_and_return:
@@ -410,432 +485,107 @@ unlock_and_return:
 }
 
 static int journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
-						   off_t offset, struct fuse_file_info *fi,
-						   enum fuse_readdir_flags flags) 
+			   off_t offset, struct fuse_file_info *fi,
+			   enum fuse_readdir_flags flags)
 {
 
 	pthread_mutex_lock(&entries_mutex);
-	if (strcmp(path, "/") != 0)
-	{
-        	pthread_mutex_unlock(&entries_mutex);
-		return -ENOENT;
+	size_t n_keys = 0;
+	char **keys = hashmap_get_keys(entries_map, &n_keys);
+	if (!keys) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -ENOMEM;
 	}
 
-	filler(buf, ".", NULL, 0, 0);
-	filler(buf, "..", NULL, 0, 0);
-
-	for (size_t i = 0; i < entry_count; i++) 
-    	{
-		char entry_name[256];
-		snprintf(entry_name, sizeof(entry_name), "%s.txt", entries[i].filename);
-		filler(buf, entry_name, NULL, 0, 0);
+	for (size_t i = 0; i < n_keys; i++) {
+		filler(buf, keys[i], NULL, 0, 0);
 	}
 
+	hashmap_free_keys(keys, n_keys);
+
 	pthread_mutex_unlock(&entries_mutex);
 	return 0;
 }
 
-static int journal_open(const char *path, struct fuse_file_info *fi) 
+static int journal_open(const char *path, struct fuse_file_info *fi)
 {
 	pthread_mutex_lock(&entries_mutex);
-
-	for (size_t i = 0; i < entry_count; i++) 
-    	{
-		char entry_path[256];
-		snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-		if (strcmp(path, entry_path) == 0) 
-		{
-                	pthread_mutex_unlock(&entries_mutex);
-			return 0;
-		}
-	}
-    	pthread_mutex_unlock(&entries_mutex);
-	return -ENOENT;
+	LOG("Opening journal for %s", path);
+	journal_entry *entry = get_entry_from_path(path);
+	int ret = entry ? 0 : -ENOENT;
+	pthread_mutex_unlock(&entries_mutex);
+	return ret;
 }
 
 static int journal_read(const char *path, char *buf, size_t size, off_t offset,
-						struct fuse_file_info *fi) 
+			struct fuse_file_info *fi)
 {
-
 	int ret_code = 0;
 	pthread_mutex_lock(&entries_mutex);
+	LOG("READ on path %s", path);
 
-	for (size_t i = 0; i < entry_count; i++) 
-    	{
-		char entry_path[256];
-		snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-
-		if (strcmp(path, entry_path) == 0) 
-    		{
-			// Format timestamp
-			struct tm tm_info; 
-			localtime_r(&entries[i].created_at, &tm_info);
-			char timestamp_str[64];
-			strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt, &tm_info);
-
-			// Build full content string using snprintf(NULL, 0) to get length
-			int needed_len = (size_t)snprintf(NULL, 0, "%s\n%s\n---\n%s\n",
-									  entries[i].summary, timestamp_str, entries[i].description);
-			if (needed_len < 0)
-			{
-				ret_code = -EIO;
-        			goto unlock_and_return;
-			}
-
-			char *full_content = calloc(needed_len + 1, sizeof(char));
-			if (!full_content) 
-    			{
-				perror("Memory allocation failed");
-				ret_code = -ENOMEM;
-        			goto unlock_and_return;
-			}
-
-			snprintf(full_content, needed_len + 1, "%s\n%s\n---\n%s\n",
-					 entries[i].summary, timestamp_str, entries[i].description);
-
-
-			if (offset >= needed_len) 
-    			{
-				free(full_content);
-				//EOF
-        			ret_code = 0;
-        			goto unlock_and_return;
-			}
-
-			if (offset + size > needed_len)
-				size = needed_len - offset;
-
-			memcpy(buf, full_content + offset, size);
-
-			free(full_content);
-
-			ret_code = size;
-			goto unlock_and_return;
-		}
+	journal_entry *entry = get_entry_from_path(path);
+	if (!entry) {
+		ret_code = -ENOENT;
+		goto unlock_and_return;
 	}
 
-unlock_and_return:
-	pthread_mutex_unlock(&entries_mutex);
-	return ret_code;
-}
-
-static int journal_truncate(const char *path, off_t size, struct fuse_file_info *fi)
-{
-	(void)fi;  // Unused
-
-	// We only support truncate to zero
-	if (size != 0)
-		return -EOPNOTSUPP;
-
-	int ret_code = 0;
-	pthread_mutex_lock(&entries_mutex);
-
-	for (size_t i = 0; i < entry_count; i++) 
-	{
-		char entry_path[256];
-		snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-
-		if (strcmp(path, entry_path) == 0) 
-		{
-			// Clear summary and description
-			free(entries[i].summary);
-			entries[i].summary = strdup("Untitled");
-
-			free(entries[i].description);
-			entries[i].description = strdup("");
-
-			time_t now = time(NULL);
-			entries[i].timestamp = now;
-
-			// Format time for LAST-MODIFIED
-			struct tm *tm_info = gmtime(&now);
-			char timestamp_str[32];
-			strftime(timestamp_str, sizeof(timestamp_str), "%Y%m%dT%H%M%SZ", tm_info);
-
-			// Load original .ics file
-			char full_path[512];
-			snprintf(full_path, sizeof(full_path), "%s/%s", ICS_DIR, entries[i].filename_original);
-                        printf("Attempting to open: %s\n", full_path);
-			FILE *fp = fopen(full_path, "r+");
-                        
-			if (!fp) {
-        			ret_code = -EIO;
-        			goto unlock_and_return;
-			}
-
-			fseek(fp, 0, SEEK_END);
-			long length = ftell(fp);
-			fseek(fp, 0, SEEK_SET);
-
-			char *content = calloc(length + 1, 1);
-			if (!content) {
-				fclose(fp);
-        			ret_code = -ENOMEM;
-        			goto unlock_and_return;
-			}
-
-			fread(content, 1, length, fp);
-			fclose(fp);
-
-			char *updated = content;
-			regex_t re;
-
-			// Replace SUMMARY
-			if (regcomp(&re, "SUMMARY:[^\r\n]*", REG_EXTENDED) == 0) {
-				regmatch_t match;
-				if (regexec(&re, content, 1, &match, 0) == 0) {
-					size_t pre = match.rm_so;
-					size_t post = strlen(content) - match.rm_eo;
-					size_t new_len = pre + 8 + strlen("Untitled") + post + 1;
-					updated = malloc(new_len);
-					snprintf(updated, new_len, "%.*sSUMMARY:Untitled%s",
-							 (int)pre, content, content + match.rm_eo);
-					free(content);
-					content = updated;
-				}
-				regfree(&re);
-			}
-
-			// Replace DESCRIPTION
-			if (regcomp(&re, "DESCRIPTION:[^\r\n]*", REG_EXTENDED) == 0) {
-				regmatch_t match;
-				if (regexec(&re, content, 1, &match, 0) == 0) {
-					size_t pre = match.rm_so;
-					size_t post = strlen(content) - match.rm_eo;
-					size_t new_len = pre + strlen("DESCRIPTION:") + post + 1;
-					updated = malloc(new_len);
-					snprintf(updated, new_len, "%.*sDESCRIPTION:%s",
-							 (int)pre, content, content + match.rm_eo);
-					free(content);
-					content = updated;
-				}
-				regfree(&re);
-			}
-
-			// Replace LAST-MODIFIED
-			if (regcomp(&re, "LAST-MODIFIED:[^\r\n]*", REG_EXTENDED) == 0) {
-				regmatch_t match;
-				if (regexec(&re, content, 1, &match, 0) == 0) {
-					size_t pre = match.rm_so;
-					size_t post = strlen(content) - match.rm_eo;
-					size_t new_len = pre + strlen(timestamp_str) + post + 16;
-					updated = malloc(new_len);
-					snprintf(updated, new_len, "%.*sLAST-MODIFIED:%s%s",
-							 (int)pre, content, timestamp_str, content + match.rm_eo);
-					free(content);
-					content = updated;
-				}
-				regfree(&re);
-			}
-
-			// Write back to file
-			fp = fopen(full_path, "w");
-			if (!fp) {
-				free(content);
-        			ret_code = -EIO;
-        			goto unlock_and_return;
-			}
-
-			fwrite(content, 1, strlen(content), fp);
-			fclose(fp);
-			free(content);
-
-			ret_code = 0;
-			goto unlock_and_return;
-		}
+	// Format timestamp
+	struct tm tm_info;
+	localtime_r(&entry->created_at, &tm_info);
+	char timestamp_str[64];
+	strftime(timestamp_str, sizeof(timestamp_str), display_date_fmt,
+		 &tm_info);
+
+	// Calculate required length for full content
+	int needed_len =
+	    (size_t)snprintf(NULL, 0, "%s\n%s\n---\n%s\n", entry->summary,
+			     timestamp_str, entry->description);
+	if (needed_len < 0) {
+		ret_code = -EIO;
+		goto unlock_and_return;
 	}
 
-	ret_code = -ENOENT;
-
-unlock_and_return:
-	pthread_mutex_unlock(&entries_mutex);
-	return ret_code;
-
-}
-
-static int journal_write(const char *path, const char *buf, size_t size,
-			 off_t offset, struct fuse_file_info *fi)
-{
-	(void)fi;  // unused
-
-	int ret_code = 0;
-	pthread_mutex_lock(&entries_mutex);
-
-	for (size_t i = 0; i < entry_count; i++) 
-	{
-		char entry_path[256];
-		snprintf(entry_path, sizeof(entry_path), "/%s.txt", entries[i].filename);
-
-		if (strcmp(path, entry_path) == 0) 
-		{
-			// Copy buffer into a temporary string for parsing
-			char *temp_buf = strndup(buf, size);
-			if (!temp_buf) 
-			{
-            			ret_code = -ENOMEM;
-            			goto unlock_and_return;
-			}
-
-			// Split into lines
-			char *summary = strtok(temp_buf, "\n");
-			char *line;
-			char *description = NULL;
-
-			while ((line = strtok(NULL, "\n")) != NULL) {
-				if (strcmp(line, "---") == 0) {
-					description = strtok(NULL, "");
-					break;
-				}
-			}
-
-			if (!summary || !description) {
-				free(temp_buf);
-            			ret_code = -EINVAL;
-            			goto unlock_and_return;
-			}
-
-			// Escape summary and description
-			char *escaped_summary = escape_text(summary);
-			char *escaped_description = escape_text(description);
-			if (!escaped_summary || !escaped_description) {
-				free(temp_buf);
-            			ret_code = -ENOMEM;
-            			goto unlock_and_return;
-			}
-
-			// Escape per RFC 5545
-			for (char *p = escaped_summary; *p; p++) {
-				if (*p == '\n') *p = ' ';
-			}
-
-			// Get timestamp
-			time_t now = time(NULL);
-			struct tm *tm_info = gmtime(&now);
-			char timestamp_str[32];
-			strftime(timestamp_str, sizeof(timestamp_str), "%Y%m%dT%H%M%SZ", tm_info);
-
-			// Load original file content
-			char full_path[512];
-			snprintf(full_path, sizeof(full_path), "%s/%s", ICS_DIR, entries[i].filename_original);
-                        printf("Attempting to open: %s\n", full_path);
-
-			FILE *fp = fopen(full_path, "r+");
-			if (!fp) {
-				perror("Failed to open file for writing");
-				free(temp_buf);
-            			ret_code = -EIO;
-            			goto unlock_and_return;
-			}
-
-			// Read original content
-			fseek(fp, 0, SEEK_END);
-			long length = ftell(fp);
-			fseek(fp, 0, SEEK_SET);
-			char *original = calloc(length + 1, 1);
-			if (!original) {
-				fclose(fp);
-				free(temp_buf);
-            			ret_code = -ENOMEM;
-            			goto unlock_and_return;
-			}
-			fread(original, 1, length, fp);
-			fclose(fp);
-
-			// Replace the fields using regex
-			regex_t re;
-			char *updated = NULL;
-
-			// Replace SUMMARY
-			if (regcomp(&re, "SUMMARY:[^\r\n]*", REG_EXTENDED) == 0) {
-				regmatch_t match;
-				if (regexec(&re, original, 1, &match, 0) == 0) {
-					size_t pre_len = match.rm_so;
-					size_t post_len = strlen(original) - match.rm_eo;
-					size_t new_len = pre_len + strlen(escaped_summary) + post_len + 8;
-					updated = malloc(new_len);
-					snprintf(updated, new_len, "%.*sSUMMARY:%s%s", 
-							 (int)pre_len, original, escaped_summary, original + match.rm_eo);
-					free(original);
-					original = updated;
-				}
-				regfree(&re);
-			}
-
-			// Replace DESCRIPTION
-			if (regcomp(&re, "DESCRIPTION:[^\r\n]*", REG_EXTENDED) == 0) {
-				regmatch_t match;
-				if (regexec(&re, original, 1, &match, 0) == 0) {
-					size_t pre_len = match.rm_so;
-					size_t post_len = strlen(original) - match.rm_eo;
-					size_t new_len = pre_len + strlen(escaped_description) + post_len + 13;
-					updated = malloc(new_len);
-					snprintf(updated, new_len, "%.*sDESCRIPTION:%s%s", 
-							 (int)pre_len, original, escaped_description, original + match.rm_eo);
-					free(original);
-					original = updated;
-				}
-				regfree(&re);
-			}
-
-			// Replace LAST-MODIFIED
-			if (regcomp(&re, "LAST-MODIFIED:[^\r\n]*", REG_EXTENDED) == 0) {
-				regmatch_t match;
-				if (regexec(&re, original, 1, &match, 0) == 0) {
-					size_t pre_len = match.rm_so;
-					size_t post_len = strlen(original) - match.rm_eo;
-					size_t new_len = pre_len + strlen(timestamp_str) + post_len + 20;
-					updated = malloc(new_len);
-					snprintf(updated, new_len, "%.*sLAST-MODIFIED:%s%s", 
-							 (int)pre_len, original, timestamp_str, original + match.rm_eo);
-					free(original);
-					original = updated;
-				}
-				regfree(&re);
-			}
-
-			// Write updated content back
-			fp = fopen(full_path, "w");
-			if (!fp) {
-				free(original);
-        			ret_code = -EIO;
-        			goto unlock_and_return;
-			}
-			fwrite(original, 1, strlen(original), fp);
-			fclose(fp);
+	char *full_content = calloc(needed_len + 1, sizeof(char));
+	if (!full_content) {
+		perror("Memory allocation failed");
+		ret_code = -ENOMEM;
+		goto unlock_and_return;
+	}
 
-			// Update in-memory values
-			free(entries[i].summary);
-			entries[i].summary = strdup(escaped_summary);
+	snprintf(full_content, needed_len + 1, "%s\n%s\n---\n%s\n",
+		 entry->summary, timestamp_str, entry->description);
 
-			free(entries[i].description);
-			entries[i].description = strdup(escaped_description);
+	if (offset >= needed_len) {
+		// EOF
+		free(full_content);
+		ret_code = 0;
+		goto unlock_and_return;
+	}
 
-			entries[i].timestamp = now;
+	if (offset + size > (size_t)needed_len)
+		size = needed_len - offset;
 
-			free(temp_buf);
-			free(escaped_summary);
-			free(escaped_description);
-			free(original);
-			ret_code = size;
-			goto unlock_and_return;
-		}
-	}
+	memcpy(buf, full_content + offset, size);
 
-	ret_code = -ENOENT;
+	free(full_content);
+	ret_code = size;
 
 unlock_and_return:
 	pthread_mutex_unlock(&entries_mutex);
 	return ret_code;
 }
 
-void *watch_ics_dir(void *arg) {
+void *watch_ics_dir(void *arg)
+{
 	int fd = inotify_init1(IN_NONBLOCK);
 	if (fd < 0) {
 		perror("inotify_init");
 		return NULL;
 	}
 
-	int wd = inotify_add_watch(fd, ICS_DIR, IN_CREATE | IN_DELETE | IN_MODIFY);
+	int wd =
+	    inotify_add_watch(fd, ICS_DIR, IN_CREATE | IN_DELETE | IN_MODIFY);
 	if (wd < 0) {
 		perror("inotify_add_watch");
 		close(fd);
@@ -857,18 +607,19 @@ void *watch_ics_dir(void *arg) {
 
 		size_t i = 0;
 		while (i < (size_t)length) {
-			struct inotify_event *event = (struct inotify_event *)&buffer[i];
+			struct inotify_event *event =
+			    (struct inotify_event *)&buffer[i];
 
 			if (event->len && strstr(event->name, ".ics")) {
-				printf("Detected change in ICS file: %s\n", event->name);
-				load_journal_entries();  // Reload entries on any change
-				break;  // Avoid excessive reloads in batch events
+				printf("Detected change in ICS file: %s\n",
+				       event->name);
+				load_journal_entries(); // Reload entries on any
+							// change
+				break;
 			}
 
 			i += sizeof(struct inotify_event) + event->len;
 		}
-
-		sleep(1);  // Debounce for a second
 	}
 
 	inotify_rm_watch(fd, wd);
@@ -876,31 +627,162 @@ void *watch_ics_dir(void *arg) {
 	return NULL;
 }
 
-static const struct fuse_operations journal_oper = 
+char *journal_entry_to_ical(const journal_entry *entry)
+{
+	char *escaped_summary =
+	    escape_text(entry->summary ? entry->summary : "");
+	char *escaped_description =
+	    escape_text(entry->description ? entry->description : "");
+
+	if (!escaped_summary || !escaped_description) {
+		free(escaped_summary);
+		free(escaped_description);
+		LOG("FAILED TO ESCAPE");
+		return NULL;
+	}
+
+	// Format timestamps
+	struct tm created_tm, modified_tm;
+	gmtime_r(&entry->created_at, &created_tm);
+	gmtime_r(&entry->timestamp, &modified_tm);
+
+	char dtstamp[32], lastmod[32];
+	strftime(dtstamp, sizeof(dtstamp), "%Y%m%dT%H%M%SZ", &created_tm);
+	strftime(lastmod, sizeof(lastmod), "%Y%m%dT%H%M%SZ", &modified_tm);
+
+	// Estimate total size (safe upper bound)
+	size_t total_size = strlen(escaped_summary) +
+			    strlen(escaped_description) + strlen(entry->uid) +
+			    strlen(dtstamp) + strlen(lastmod) +
+			    512; // Extra for formatting
+
+	char *ical = malloc(total_size);
+	if (!ical) {
+		free(escaped_summary);
+		free(escaped_description);
+		return NULL;
+	}
+
+	snprintf(ical, total_size,
+		 "BEGIN:VCALENDAR\n"
+		 "BEGIN:VJOURNAL\n"
+		 "UID:%s\n"
+		 "DTSTAMP:%s\n"
+		 "LAST-MODIFIED:%s\n"
+		 "SUMMARY:%s\n"
+		 "DESCRIPTION:%s\n"
+		 "END:VJOURNAL\n"
+		 "END:VCALENDAR\n",
+		 entry->uid, dtstamp, lastmod, escaped_summary,
+		 escaped_description);
+
+	free(escaped_summary);
+	free(escaped_description);
+	return ical;
+}
+
+static int journal_write(const char *path, const char *buf, size_t size,
+			 off_t offset, struct fuse_file_info *fi)
 {
-	.getattr 	= journal_getattr,
-	.readdir 	= journal_readdir,
-	.open		= journal_open,
-	.read		= journal_read,
-	.write		= journal_write,
-	.truncate	= journal_truncate 
+	pthread_mutex_lock(&entries_mutex);
+	journal_entry *entry = get_entry_from_path(path);
+	if (!entry) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -ENOENT;
+	}
+
+	// Only allow overwrite of the full file for simplicity
+	if (offset != 0) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -EINVAL;
+	}
+
+	// Make a copy of the input buffer and null-terminate
+	char *content = strndup(buf, size);
+	if (!content) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -ENOMEM;
+	}
+
+	// Parse new content: "<summary>\n<timestamp>\n---\n<description>"
+	char *summary = NULL, *description = NULL;
+
+	char *sep = strstr(content, "---\n");
+	if (!sep) {
+		free(content);
+		pthread_mutex_unlock(&entries_mutex);
+		return -EINVAL; // Bad format
+	}
+
+	*sep = '\0';
+	description = sep + 4;
+
+	// Split summary and timestamp
+	char *newline = strchr(content, '\n');
+	if (!newline) {
+		free(content);
+		pthread_mutex_unlock(&entries_mutex);
+		return -EINVAL; // Missing timestamp
+	}
+
+	*newline = '\0';
+	summary = content;
+
+	// Replace in-memory values
+	free(entry->summary);
+	free(entry->description);
+	entry->summary = strdup(summary);
+	entry->description = strdup(description);
+	entry->timestamp = time(NULL);
+
+	// Write back to original .ics file
+	char filepath[512];
+	snprintf(filepath, sizeof(filepath), "%s/%s", ICS_DIR,
+		 entry->filename_original);
+
+	char *ical_str = journal_entry_to_ical(entry);
+	if (!ical_str) {
+		pthread_mutex_unlock(&entries_mutex);
+		return -EIO;
+	}
+
+	FILE *f = fopen(filepath, "w");
+	if (!f) {
+		perror("Failed to open ICS file");
+		free(ical_str);
+		pthread_mutex_unlock(&entries_mutex);
+		return -EIO;
+	}
+
+	fputs(ical_str, f);
+	fclose(f);
+	free(ical_str);
+	pthread_mutex_unlock(&entries_mutex);
+	return size;
+}
+
+static const struct fuse_operations journal_oper = {
+    .getattr = journal_getattr,
+    .readdir = journal_readdir,
+    .open = journal_open,
+    .read = journal_read,
+    .write = journal_write,
 };
 
-int main(int argc, char *argv[]) 
-{    
-    pthread_t watcher_thread;
-    load_journal_entries();
+int main(int argc, char *argv[])
+{
+	// pthread_t watcher_thread;
+	load_journal_entries();
 
-    if (pthread_create(&watcher_thread, NULL, watch_ics_dir, NULL) != 0) {
-        perror("Failed to create inotify watcher thread");
-        return 1;
-    }
+	// if (pthread_create(&watcher_thread, NULL, watch_ics_dir, NULL) != 0)
+	// { perror("Failed to create inotify watcher thread"); return 1;
+	//}
 
-    int ret = fuse_main(argc, argv, &journal_oper, NULL);
+	int ret = fuse_main(argc, argv, &journal_oper, NULL);
 
-    pthread_cancel(watcher_thread);  // Stop the watcher when FUSE ends
-    pthread_join(watcher_thread, NULL);  // Clean up
+	// pthread_cancel(watcher_thread);  // Stop the watcher when FUSE ends
+	// pthread_join(watcher_thread, NULL);  // Clean up
 
-    free_all_entries();
-    return ret;
+	free_all_entries();
+	return ret;
 }