agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 27ec9083093cf039d9b0412b3333df16d2bfe4ec
parent 5622b3d0f8743e8faceef9ae6edeeda35235ed9a
Author: Marc Coquand <marc@coquand.email>
Date:   Thu,  5 Jun 2025 20:08:54 +0100

Add slightly hacky restructure to handle today vs journal notes

Later on we should have a more robust lookup solution for these
symlinks, as they will have subdirectories and alternative names.

Diffstat:
Mmain.c | 196++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
1 file changed, 134 insertions(+), 62 deletions(-)
diff --git a/main.c b/main.c
@@ -91,26 +91,38 @@ journal_entry *
 get_entry_from_fuse_path(const char *path)
 {
 	if (!path || path[0] != '/') {
-		LOG("invalid path '%s'\n", path ? path : "NULL");
+		LOG("Invalid path: '%s'\n", path ? path : "NULL");
 		return NULL;
 	}
 
-	const char *key = path + 1;
+	const char *prefix = "/journal/";
+	size_t prefix_len = strlen(prefix);
+
+	// Must start with "/journal/"
+	if (strncmp(path, prefix, prefix_len) != 0) {
+		LOG("Path does not start with '/journal/': '%s'\n", path);
+		return NULL;
+	}
+
+	// Extract the part after "/journal/"
+	const char *key = path + prefix_len;
+
+	// If key is empty (i.e., path is exactly "/journal/"), reject
 	if (strlen(key) == 0) {
-		LOG("empty key for path '%s'\n", path);
+		LOG("Empty key in path: '%s'\n", path);
 		return NULL;
 	}
 
-	LOG("looking up key '%s'\n", key);
+	LOG("Looking up key: '%s'\n", key);
 	journal_entry *entry = hashmap_get(entries_fuse_key, key);
 
 	if (!entry) {
-		LOG("no entry found for key '%s'\n", key);
+		LOG("No entry found for key: '%s'\n", key);
 		return NULL;
 	}
 
-	LOG("Entry is %s", entry->filename);
-	LOG("Entry original is %s", entry->filename_original);
+	LOG("Entry found: filename = '%s', original = '%s'\n", entry->filename,
+	    entry->filename_original);
 	return entry;
 }
 
@@ -177,43 +189,6 @@ time_string(icaltimetype t, const char *format)
 	return buffer;
 }
 
-// Uses created_at as filename.
-// However, in case of collission, set the add 1,2 et.c.
-char *
-get_unique_filename(journal_entry *entry, struct hashmap *entries_map)
-{
-	icalcomponent *c = entry->component;
-	icaltimetype created_at = icalcomponent_get_dtstamp(c);
-
-	char *filename_time_str = time_string(created_at, date_title_fmt);
-	if (!filename_time_str) {
-		LOG("Failed to format date string");
-		return NULL;
-	}
-
-	// Attempt filename and increment suffix if needed
-	char *unique_filename = NULL;
-	if (asprintf(&unique_filename, "%s.txt", filename_time_str) == -1) {
-		free(filename_time_str);
-		return NULL;
-	}
-
-	// In the rare cases that some entry is created on the same minute.
-	int suffix = 1;
-	while (hashmap_get(entries_map, unique_filename) != NULL) {
-		suffix++;
-		free(unique_filename);
-		if (asprintf(&unique_filename, "%s_%d.txt", filename_time_str,
-			     suffix) == -1) {
-			return NULL;
-		}
-	}
-
-	free(filename_time_str);
-
-	return unique_filename;
-}
-
 // Returns -1 on failure, 0 on success
 int
 load_journal_entry(char *filename, journal_entry *entry)
@@ -240,16 +215,11 @@ load_journal_entry(char *filename, journal_entry *entry)
 
 	// Make sure filename, which happens when
 	// Two entries are created on the same minute
-	char *unique_filename = get_unique_filename(entry, entries_fuse_key);
-	if (!unique_filename) {
-		LOG("Failed to generate filename for %s", filename);
-
-		free(filepath);
+	if (asprintf(&entry->filename, "%s.txt",
+		     icalcomponent_get_uid(entry->component)) == -1) {
+		LOG("Failed to write filename");
 		return -1;
-	}
-
-	entry->filename = unique_filename;
-	LOG("Got unique filename %s", unique_filename);
+	};
 
 	free(filepath);
 	return 0;
@@ -359,11 +329,45 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 	int ret_code = 0;
 
 	if (strcmp(path, "/") == 0) {
-		stbuf->st_mode = S_IFDIR | 0755;
+		stbuf->st_mode = S_IFDIR | 0444;
+		stbuf->st_nlink = 2;
+		goto unlock_and_return;
+	}
+
+	if (strcmp(path, "/journal") == 0) {
+		stbuf->st_mode = S_IFDIR | 0444;
+		stbuf->st_nlink = 2;
+		goto unlock_and_return;
+	}
+
+	if (strcmp(path, "/today") == 0) {
+		stbuf->st_mode = S_IFDIR | 0444;
 		stbuf->st_nlink = 2;
 		goto unlock_and_return;
 	}
 
+	const char *today_prefix = "/today/";
+	if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
+		const char *key = path + strlen(today_prefix);
+		journal_entry *entry = hashmap_get(entries_fuse_key, key);
+
+		if (!entry) {
+			ret_code = -ENOENT;
+			goto unlock_and_return;
+		}
+
+		icalcomponent *component = entry->component;
+		struct icaltimetype dt = icalcomponent_get_dtstamp(component);
+		if (icaltime_compare_date_only(dt, icaltime_today()) != 0) {
+			ret_code = -ENOENT;
+			goto unlock_and_return;
+		}
+
+		stbuf->st_mode = S_IFLNK | 0444;
+		stbuf->st_nlink = 1;
+		goto unlock_and_return;
+	}
+
 	journal_entry *entry = get_entry_from_fuse_path(path);
 	if (!entry) {
 		LOG("Entry not found");
@@ -394,6 +398,28 @@ unlock_and_return:
 }
 
 int
+journal_readlink(const char *path, char *buf, size_t size)
+{
+	LOG("READLINK path: %s", path);
+	const char *today_prefix = "/today/";
+	if (strncmp(path, today_prefix, strlen(today_prefix)) == 0) {
+		pthread_mutex_lock(&entries_mutex);
+		const char *key = path + strlen(today_prefix);
+		LOG("Lookup %s", key);
+		journal_entry *entry = hashmap_get(entries_fuse_key, key);
+
+		if (!entry)
+			return -ENOENT;
+
+		// Return symlink target
+		snprintf(buf, size, "../journal/%s", key);
+
+		pthread_mutex_unlock(&entries_mutex);
+		return 0;
+	}
+	return -ENOENT;
+}
+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)
@@ -401,20 +427,62 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 
 	pthread_mutex_lock(&entries_mutex);
 	LOG("READDIR %s", path);
+	filler(buf, ".", NULL, 0, 0);
+	filler(buf, "..", NULL, 0, 0);
 
-	size_t n_keys = 0;
-	char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
-	if (!keys) {
-		pthread_mutex_unlock(&entries_mutex);
-		return -ENOMEM;
+	// If we are in the root directory, expose the "journal" directory
+	if (strcmp(path, "/") == 0) {
+		filler(buf, "journal", NULL, 0, 0);
+		filler(buf, "today", NULL, 0, 0);
 	}
+	else if (strcmp(path, "/journal") == 0) {
+		size_t n_keys = 0;
+		char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
+		if (!keys) {
+			pthread_mutex_unlock(&entries_mutex);
+			return -ENOMEM;
+		}
+
+		for (size_t i = 0; i < n_keys; i++) {
+			filler(buf, keys[i], 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);
 	}
+	else if (strcmp(path, "/today") == 0) {
+		time_t now = time(NULL);
+		struct tm *now_tm = localtime(&now);
+
+		size_t n_keys = 0;
+		char **keys = hashmap_get_keys(entries_fuse_key, &n_keys);
+		if (!keys) {
+			pthread_mutex_unlock(&entries_mutex);
+			return -ENOMEM;
+		}
 
-	hashmap_free_keys(keys, n_keys);
+		for (size_t i = 0; i < n_keys; i++) {
+			journal_entry *entry =
+			    hashmap_get(entries_fuse_key, keys[i]);
+			if (!entry)
+				continue;
+
+			// Get the dtstamp of the first icalcomponent
+			icalcomponent *component = entry->component;
+			if (!component)
+				continue;
+
+			struct icaltimetype dt =
+			    icalcomponent_get_dtstamp(component);
+			if (dt.year == (now_tm->tm_year + 1900) &&
+			    dt.month == (now_tm->tm_mon + 1) &&
+			    dt.day == now_tm->tm_mday) {
+				filler(buf, keys[i], NULL, 0,
+				       0); // same name as in /journal
+			}
+		}
 
+		hashmap_free_keys(keys, n_keys);
+	}
 	pthread_mutex_unlock(&entries_mutex);
 	return 0;
 }
@@ -529,6 +597,8 @@ write_entry_to_ical_file(const journal_entry *entry)
 		icalproperty_set_lastmodified(prop, get_ical_now());
 	}
 
+	icalcomponent_set_status(entry->component, ICAL_STATUS_FINAL);
+
 	LOG("Set last modified");
 
 	char *ical_str = icalcomponent_as_ical_string_r(entry->component);
@@ -945,6 +1015,8 @@ static const struct fuse_operations journal_oper = {.getattr = journal_getattr,
 						    .write = journal_write,
 						    .create = journal_create,
 						    .unlink = journal_unlink,
+						    .readlink =
+							journal_readlink,
 						    .rename = journal_rename};
 
 size_t