agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit b04e20707adcb9b5ef486f83f3a3dc076a0f81d2
parent e746486aefcd47399704b99a3c664e72a44f0578
Author: Marc Coquand <marc@coquand.email>
Date:   Wed,  9 Jul 2025 23:38:27 +0200

Implement get category property with xattr

Still missing set and delete.

Implementation uses memstreams to make it simpler to append to
string

References: https://todo.sr.ht/~marcc/agendafs/1

Diffstat:
Mjournal_entry.c | 82+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Mjournal_entry.h | 5++++-
Mmain.c | 31++++++++++++++++++++++++++++---
3 files changed, 102 insertions(+), 16 deletions(-)
diff --git a/journal_entry.c b/journal_entry.c
@@ -235,7 +235,7 @@ free_segments(char **segments, size_t count)
 }
 
 struct tree_node *
-get_node_by_path(struct tree_node *root, const char *path)
+get_node_by_path(const char *path)
 {
 	if (strcmp(path, "/") == 0) {
 		LOG("Is ROOT %s", path);
@@ -248,7 +248,7 @@ get_node_by_path(struct tree_node *root, const char *path)
 		return fuse_tree_root;
 	LOG("Segments are defined");
 
-	struct tree_node *current = root;
+	struct tree_node *current = fuse_tree_root;
 
 	for (size_t i = 0; i < count && current; ++i) {
 		const char *segment = segments[i];
@@ -317,7 +317,7 @@ get_entry_from_fuse_path(const char *path)
 		LOG("Invalid path: '%s'\n", path ? path : "NULL");
 		return NULL;
 	}
-	const struct tree_node *node = get_node_by_path(fuse_tree_root, path);
+	const struct tree_node *node = get_node_by_path(path);
 
 	if (!node || !node->data) {
 		LOG("No entry found for key: '%s'\n", path);
@@ -723,6 +723,67 @@ copy_journal_entry(const struct journal_entry *src)
 	return copy;
 }
 
+static int
+append_category_to_memstream(FILE *memstream, const char *category,
+			     bool is_first_category)
+{
+	if (!is_first_category) {
+		if (fputs(",", memstream) == EOF) {
+			LOG("FPUTS FAILED");
+			return -1;
+		}
+	}
+
+	if (fputs(category, memstream) == EOF) {
+		LOG("FPUTS FAILED");
+		return -1;
+	}
+	return 0; // Success
+}
+
+// Returns comma separated list of categories for a file
+const char *
+get_node_categories(const struct tree_node *node)
+{
+	const struct journal_entry *entry = node->data;
+	if (!entry) {
+		return "\0";
+	}
+
+	FILE *memstream = NULL;
+	char *result_buffer = NULL;
+	size_t buffer_size = 0;
+
+	memstream = open_memstream(&result_buffer, &buffer_size);
+
+	icalcomponent *inner = icalcomponent_get_inner(entry->component);
+
+	icalproperty *catp =
+	    icalcomponent_get_first_property(inner, ICAL_CATEGORIES_PROPERTY);
+
+	bool first_category = true;
+	while (catp) {
+		const char *category = icalproperty_get_categories(catp);
+		if (append_category_to_memstream(memstream, category,
+						 first_category) == -1) {
+			fclose(memstream);
+			free(result_buffer);
+			return "";
+		}
+		catp = icalcomponent_get_next_property(
+		    inner, ICAL_CATEGORIES_PROPERTY);
+
+		first_category = false;
+	}
+
+	if (fclose(memstream) == EOF) {
+		free(result_buffer);
+		return NULL; // Error during close
+	}
+
+	return result_buffer;
+}
+
 size_t
 write_entry_to_ical_file(const struct journal_entry *entry)
 {
@@ -802,8 +863,7 @@ do_journal_entry_rename(const char *old, const char *new)
 	char *parent_path = NULL;
 	get_parent_path(new, &parent_path);
 	LOG("PARENT PATH: %s", parent_path);
-	struct tree_node *new_parent_node =
-	    get_node_by_path(fuse_tree_root, parent_path);
+	struct tree_node *new_parent_node = get_node_by_path(parent_path);
 	free(parent_path);
 
 	if (new_parent_node == NULL) {
@@ -820,7 +880,7 @@ do_journal_entry_rename(const char *old, const char *new)
 		*dot = '\0';
 	}
 	LOG("Summary is now %s", new_summary);
-	struct tree_node *entry_node = get_node_by_path(fuse_tree_root, old);
+	struct tree_node *entry_node = get_node_by_path(old);
 	if (!entry_node) {
 		free(new_copy);
 		free(new_summary);
@@ -998,8 +1058,7 @@ create_entry_from_fuse(const char *fuse_path)
 	char *parent_path = NULL;
 	get_parent_path(fuse_path, &parent_path);
 	LOG("Parent path is %s", parent_path);
-	struct tree_node *parent =
-	    get_node_by_path(fuse_tree_root, parent_path);
+	struct tree_node *parent = get_node_by_path(parent_path);
 	free(parent_path);
 
 	if (!parent) {
@@ -1055,8 +1114,7 @@ create_directory_from_fuse_path(const char *fuse_path)
 	char *parent_path = NULL;
 	get_parent_path(fuse_path, &parent_path);
 	LOG("Parent path is %s", parent_path);
-	struct tree_node *parent =
-	    get_node_by_path(fuse_tree_root, parent_path);
+	struct tree_node *parent = get_node_by_path(parent_path);
 	free(parent_path);
 
 	if (!parent) {
@@ -1142,7 +1200,7 @@ int
 delete_dir_from_fuse_path(const char *filepath)
 {
 	int res = 0;
-	struct tree_node *node = get_node_by_path(fuse_tree_root, filepath);
+	struct tree_node *node = get_node_by_path(filepath);
 	if (!node) {
 		LOG("Node not found");
 		return -EIO;
@@ -1180,7 +1238,7 @@ int
 delete_from_fuse_path(const char *filepath)
 {
 	int res = 0;
-	struct tree_node *node = get_node_by_path(fuse_tree_root, filepath);
+	struct tree_node *node = get_node_by_path(filepath);
 	if (!node) {
 		return -EIO;
 	}
diff --git a/journal_entry.h b/journal_entry.h
@@ -134,7 +134,10 @@ int
 build_today_hashmap();
 
 struct tree_node *
-get_node_by_path(struct tree_node *root, const char *path);
+get_node_by_path(const char *path);
+
+const char *
+get_node_categories(const struct tree_node *);
 
 int
 build_notes_hashmap();
diff --git a/main.c b/main.c
@@ -47,7 +47,7 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 		goto unlock_and_return;
 	}
 
-	const struct tree_node *node = get_node_by_path(fuse_tree_root, path);
+	const struct tree_node *node = get_node_by_path(path);
 	if (!node) {
 		LOG("Entry not found");
 		ret_code = -ENOENT;
@@ -131,8 +131,7 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 		}
 	}
 	else {
-		const struct tree_node *node =
-		    get_node_by_path(fuse_tree_root, path);
+		const struct tree_node *node = get_node_by_path(path);
 		if (node) {
 			for (size_t i = 0; i < node->child_count; i++) {
 				const struct tree_node *child =
@@ -415,6 +414,29 @@ journal_utimens(const char *path, const struct timespec tv[2],
 	return 0;
 }
 
+static int
+journal_getxattr(const char *path, const char *header, char *buf, size_t s)
+{
+	LOG("GETXATTR '%s' '%s' '%zu'\n", path, header, s);
+
+	if (strcmp(header, "user.categories") != 0) {
+		return -ENODATA;
+	}
+
+	struct tree_node *node = get_node_by_path(path);
+
+	const char *cats = get_node_categories(node);
+
+	int string_len = strlen(cats);
+
+	// If s == 0 we don't need to write, just return size to allocate
+	if (s > 0) {
+		snprintf(buf, s, "%s", cats);
+	}
+
+	return string_len;
+}
+
 struct fuse_operations journal_oper = {.getattr = journal_getattr,
 				       .readdir = journal_readdir,
 				       .open = journal_open,
@@ -424,6 +446,7 @@ struct fuse_operations journal_oper = {.getattr = journal_getattr,
 				       .create = journal_create,
 				       .mkdir = journal_mkdir,
 				       .unlink = journal_unlink,
+				       .getxattr = journal_getxattr,
 				       .rmdir = journal_rmdir,
 				       .readlink = journal_readlink,
 				       .rename = journal_rename};
@@ -505,6 +528,8 @@ main(int argc, char *argv[])
 		exit(1);
 	}
 
+	LOG("LOADED ICS DIR");
+
 	if (pthread_create(&watcher_thread, NULL, watch_ics_dir, NULL) != 0) {
 		perror("Failed to create inotify watcher thread");
 		return 1;