agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 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:
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);