agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit f145db7a2f72164f2ac5aa5746ac05cf8efb7147
parent 889d5554b05d183669d14b60a7baf45fda8de5c9
Author: Marc Coquand <marc@coquand.email>
Date:   Sun, 17 Aug 2025 10:42:54 +0200

Add dtstart support

Implements: https://todo.sr.ht/~marcc/agendafs/3

Diffstat:
Magendafs.8.scd | 12++++++++++++
Mfuse_node.c | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mfuse_node.h | 9+++++++++
Mmain.c | 35+++++++++++++++++++++++++++++++++--
4 files changed, 101 insertions(+), 2 deletions(-)
diff --git a/agendafs.8.scd b/agendafs.8.scd
@@ -47,6 +47,10 @@ usage. Built-in extended attributes are:
 	Immutable. The underlying UUID for the file. In the future, this
 	can be used to reference notes with *user.sibling*.
 
+*user.dtstart*
+	Date-time start, ISO-formatted. Specifies the "beginning" of the
+	file.
+
 Arbitrary user-provided attributes are also supported. Limits are set to
 255 bytes for attributes and 64 kilobytes for their value.
 
@@ -94,6 +98,14 @@ Find files by category MY_CATEGORY:
 	  | awk '{ print substr($0, 9) }'
 	```
 
+The dtstart xattribute can be used for journaling. Simply set the xattribute
+when creating a new file:
+
+	```
+	$ FILE="Meeting with John" TIME=$(date +'%Y%m%d') touch "$FILE" \\
+	   && setfattr -n user.dtstart -v $TIME $FILE
+	```
+
 # SEE ALSO
 
 _pimsync_(1)
diff --git a/fuse_node.c b/fuse_node.c
@@ -337,6 +337,53 @@ load_root_node_tree()
 	LOG("Done");
 }
 
+// Sets a validated dtstart
+int
+set_dtstart(memory_region *mreg, const char *dtstart_c,
+	    const struct tree_node *node)
+{
+	struct icaltimetype dtstart = icaltime_from_string(dtstart_c);
+	if (icaltime_compare(dtstart, icaltime_null_time()) == 0) {
+		return -EINVAL;
+	}
+
+	icalcomponent *comp = get_icalcomponent_from_node(mreg, node);
+
+	icalcomponent_set_dtstart(comp, dtstart);
+
+	return write_ical_file(mreg, node, comp);
+}
+
+const char *
+get_dtstart(memory_region *mreg, struct tree_node *n)
+{
+
+	icalcomponent *comp = get_icalcomponent_from_node(mreg, n);
+
+	struct icaltimetype dtstart = icalcomponent_get_dtstart(comp);
+	if (icaltime_compare(dtstart, icaltime_null_time()) == 0) {
+		return NULL;
+	}
+
+	const char *time = icaltime_as_ical_string(dtstart);
+	return time;
+}
+
+int
+clear_dtstart(memory_region *mreg, struct tree_node *node)
+{
+	icalcomponent *comp = get_icalcomponent_from_node(mreg, node);
+
+	icalproperty *p =
+	    icalcomponent_get_first_property(comp, ICAL_DTSTART_PROPERTY);
+
+	if (p) {
+		icalcomponent_remove_property(comp, p);
+	}
+
+	return write_ical_file(mreg, node, comp);
+}
+
 static int
 append_category_to_memstream(FILE *memstream, const char *category,
 			     bool is_first_category)
diff --git a/fuse_node.h b/fuse_node.h
@@ -101,6 +101,15 @@ int
 create_entry_from_fuse(memory_region *mreg, const char *fuse_path,
 		       enum ENTRY_TYPE etype);
 
+const char *
+get_dtstart(memory_region *mreg, struct tree_node *n);
+
+int
+set_dtstart(memory_region *mreg, const char *dtstart_c,
+	    const struct tree_node *node);
+
+int
+clear_dtstart(memory_region *mreg, struct tree_node *node);
 int
 create_directory_from_fuse_path(memory_region *mreg, const char *fuse_path);
 
diff --git a/main.c b/main.c
@@ -1,6 +1,5 @@
 #define FUSE_USE_VERSION 31
 
-#include <fuse3/fuse_opt.h>
 #include "fuse_node.h"
 #include "fuse_node_store.h"
 #include "ical_extra.h"
@@ -11,6 +10,7 @@
 #include <errno.h>
 #include <fuse3/fuse.h>
 #include <fuse3/fuse_lowlevel.h>
+#include <fuse3/fuse_opt.h>
 #include <libical/ical.h>
 #include <pthread.h>
 #include <regex.h>
@@ -382,6 +382,10 @@ fuse_removexattr(const char *path, const char *attribute)
 		status = delete_node_class(mreg, node);
 		goto cleanup_return;
 	}
+	if (strcmp(attribute, "user.dtstart") == 0) {
+		status = clear_dtstart(mreg, node);
+		goto cleanup_return;
+	}
 	else if (starts_with_str(attribute, "user.")) {
 		icalcomponent *ic = get_icalcomponent_from_node(mreg, node);
 
@@ -433,7 +437,15 @@ fuse_setxattr(const char *path, const char *attribute, const char *value,
 		status = -EPERM;
 		goto cleanup_return;
 	}
-
+	if (strcmp(attribute, "user.dtstart") == 0) {
+		if (strcmp(value, "") == 0) {
+			status = clear_dtstart(mreg, node);
+		}
+		else {
+			status = set_dtstart(mreg, value, node);
+		}
+		goto cleanup_return;
+	}
 	if (strcmp(attribute, "user.categories") == 0) {
 		status = set_node_categories(mreg, node, value, s);
 		goto cleanup_return;
@@ -502,6 +514,12 @@ fuse_listxattr(const char *path, char *list, size_t size)
 		fputc('\0', stream);
 	}
 
+	const char *dtstart = get_dtstart(mreg, node);
+	if (dtstart != NULL) {
+		fprintf(stream, "user.dtstart");
+		fputc('\0', stream);
+	}
+
 	// All notes have a uid
 	fprintf(stream, "user.uid");
 	fputc('\0', stream);
@@ -594,6 +612,19 @@ fuse_getxattr(const char *path, const char *attribute, char *buf, size_t s)
 		status = string_len;
 		goto cleanup_return;
 	}
+	else if (strcmp(attribute, "user.dtstart") == 0) {
+		const char *res = get_dtstart(mreg, node);
+		if (!res) {
+			status = -ENODATA;
+			goto cleanup_return;
+		}
+
+		int string_len = strlen(res);
+		snprintf(buf, s, "%s", res);
+
+		status = string_len;
+		goto cleanup_return;
+	}
 	else if (starts_with_str(attribute, "user.")) {
 		icalcomponent *ic = get_icalcomponent_from_node(mreg, node);