agendafs

A filesystem for your calendar.

git clone git://mccd.space/agendafs
commit 4ab4a8d3921063c124b2c40179f3139d5e76d2d4
parent 1052e9696906fdcb40eb8bf92caecad178643b82
Author: Marc Coquand <marc@coquand.email>
Date:   Tue, 10 Jun 2025 12:09:43 +0100

Add notes support

Diffstat:
MREADME.md | 32++++++++++++++++++++++++++++++--
Mmain.c | 266+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mshell.nix | 1+
3 files changed, 245 insertions(+), 54 deletions(-)
diff --git a/README.md b/README.md
@@ -70,10 +70,14 @@ Early alpha. Very WIP.
 	- [x] Write
 	- [x] Set creation dates - not mandatory but supresses a few warnings
 	- [ ] Directory structure
+	- [ ] Use DTSTART;VALUE=DATE: for journal to separate them from notes.
+- [ ] A new file format.
+- [ ] Categories (Maybe symlinks?)
+- [ ] Relationships 
 - [ ] VTODO
-- [ ] Categories (either via xattr or via something else. Symlinks?)
+	- [ ] Embed in VJOURNAL using parent relationship
 - [ ] VEVENTS
-- [ ] Relationships / linking
+ 	- [ ] Design. 
 - [ ] VALARM
 - [ ] VFREEBUSY
 
@@ -90,6 +94,25 @@ like [vdirsyncer](https://vdirsyncer.pimutils.org/en/stable/when.html#).
 $ make
 ```
 
+## The file format (Draft)
+
+```
+--1 Blocks
+
+I'm thinking that the structure that would make the most
+
+sense is a block based structure.
+
+--2 [ ] Would be a todo
+--3 [ ] Would be another todo
+--4 Stiching together notes @category
+
+It'd be possible to have a subnote, which would reference this as a parent.
+
+A number beforehand would be an ID for the note, allowing you to move them around.
+```
+
+
 ## Using
 
 1. Set up a new calendar with VJOURNAL support. See below for how its done in Nextcloud.
@@ -154,6 +177,11 @@ Longer text
 The `---` are mandatory.
 
 
+## Tool-specific notes
+
+### lf
+
+Updates might not be reflected with dircache set to true.
 
 ## Why C?
 
diff --git a/main.c b/main.c
@@ -51,6 +51,9 @@ struct hashmap *entries_original_ics = NULL;
 // string -> string
 struct hashmap *entries_today = NULL;
 
+// string -> string
+struct hashmap *entries_notes = NULL;
+
 static pthread_mutex_t entries_mutex = PTHREAD_MUTEX_INITIALIZER;
 icaltimetype
 get_ical_now()
@@ -322,6 +325,124 @@ get_last_modified(journal_entry *entry)
 	}
 }
 
+// TODO: Probably better to calculate this on demand
+int
+build_today_hashmap()
+{
+	if (entries_today != NULL)
+		hashmap_free(entries_today);
+	entries_today = hashmap_new(free);
+
+	size_t n_keys = 0;
+	char **keys = hashmap_get_keys(entries_fuse, &n_keys);
+
+	if (!keys) {
+		LOG("Could not get keys");
+		return -ENOMEM;
+	}
+
+	for (size_t i = 0; i < n_keys; i++) {
+		journal_entry *entry = hashmap_get(entries_fuse, 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 (icaltime_compare_date_only(dt, icaltime_today()) == 0) {
+			const char *summary =
+			    icalcomponent_get_summary(component);
+			if (summary == NULL) {
+				LOG("SUMMARY IS NULL");
+				continue;
+			}
+			char *filename = strdup(summary);
+
+			if (filename == NULL) {
+				LOG("Could not get filename");
+				continue;
+			}
+
+			char *dest;
+
+			if (asprintf(&dest, "../entries/%s", entry->filename) ==
+			    -1)
+				return -ENOMEM;
+
+			hashmap_insert(entries_today, filename, dest);
+		}
+	}
+	LOG("Build complete");
+
+	hashmap_free_keys(keys, n_keys);
+	return 0;
+}
+
+int
+build_notes_hashmap()
+{
+	if (entries_notes != NULL)
+		hashmap_free(entries_notes);
+	entries_notes = hashmap_new(free);
+	LOG("BUILDING ENTRIES");
+
+	size_t n_keys = 0;
+	char **keys = hashmap_get_keys(entries_fuse, &n_keys);
+
+	if (!keys) {
+		LOG("Could not get keys");
+		return -ENOMEM;
+	}
+	LOG("GOT ENTRIES");
+
+	for (size_t i = 0; i < n_keys; i++) {
+		journal_entry *entry = hashmap_get(entries_fuse, keys[i]);
+
+		LOG("Looking up entry");
+		if (!entry)
+			continue;
+
+		// Get the dtstamp of the first icalcomponent
+		icalcomponent *component = entry->component;
+		if (!component)
+			continue;
+
+		icalcomponent_kind type =
+		    icalcomponent_isa(icalcomponent_get_inner(component));
+
+		if (type == ICAL_VJOURNAL_COMPONENT) {
+			const char *summary =
+			    icalcomponent_get_summary(component);
+			if (summary == NULL) {
+				LOG("SUMMARY IS NULL");
+				continue;
+			}
+			char *filename = strdup(summary);
+
+			if (filename == NULL) {
+				LOG("Could not get filename");
+				continue;
+			}
+
+			char *dest;
+
+			if (asprintf(&dest, "../entries/%s", entry->filename) ==
+			    -1)
+				return -ENOMEM;
+
+			hashmap_insert(entries_notes, filename, dest);
+		}
+	}
+	LOG("Build complete");
+
+	hashmap_free_keys(keys, n_keys);
+	return 0;
+}
+
 int
 journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 {
@@ -350,9 +471,16 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 		goto unlock_and_return;
 	}
 
+	if (strcmp(path, "/notes") == 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);
+		build_today_hashmap();
 		char *entry_filename = hashmap_get(entries_today, key);
 		if (!entry_filename) {
 			ret_code = -ENOENT;
@@ -364,6 +492,23 @@ journal_getattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi)
 		goto unlock_and_return;
 	}
 
+	const char *notes_prefix = "/notes/";
+	if (strncmp(path, notes_prefix, strlen(notes_prefix)) == 0) {
+		const char *key = path + strlen(notes_prefix);
+		LOG("NOTES");
+		build_notes_hashmap();
+		LOG("BUILT NOTES HASHMAP");
+		char *entry_filename = hashmap_get(entries_notes, key);
+		if (!entry_filename) {
+			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");
@@ -406,6 +551,8 @@ journal_readlink(const char *path, char *buf, size_t size)
 
 		if (!link) {
 			LOG("Link not found for %s", key);
+
+			pthread_mutex_unlock(&entries_mutex);
 			return -ENOENT;
 		}
 		LOG("Link found %s", link);
@@ -416,46 +563,29 @@ journal_readlink(const char *path, char *buf, size_t size)
 		pthread_mutex_unlock(&entries_mutex);
 		return 0;
 	}
-	return -ENOENT;
-}
-
-int
-buildTodayHashmap()
-{
-	hashmap_free(entries_today);
-	entries_today = hashmap_new(free);
-
-	size_t n_keys = 0;
-	char **keys = hashmap_get_keys(entries_fuse, &n_keys);
-	if (!keys) {
-		return -ENOMEM;
-	}
+	const char *notes_prefix = "/notes/";
+	if (strncmp(path, notes_prefix, strlen(notes_prefix)) == 0) {
+		pthread_mutex_lock(&entries_mutex);
+		const char *key = path + strlen(notes_prefix);
+		LOG("Lookup %s", key);
+		char *link = hashmap_get(entries_notes, key);
 
-	for (size_t i = 0; i < n_keys; i++) {
-		journal_entry *entry = hashmap_get(entries_fuse, keys[i]);
-		if (!entry)
-			continue;
+		if (!link) {
+			LOG("Link not found for %s", key);
 
-		// Get the dtstamp of the first icalcomponent
-		icalcomponent *component = entry->component;
-		if (!component)
-			continue;
+			pthread_mutex_unlock(&entries_mutex);
+			return -ENOENT;
+		}
+		LOG("Link found %s", link);
 
-		struct icaltimetype dt = icalcomponent_get_dtstamp(component);
+		// Return symlink target
+		snprintf(buf, size, "%s", link);
 
-		if (icaltime_compare_date_only(dt, icaltime_today()) == 0) {
-			char *filename =
-			    strdup(icalcomponent_get_summary(component));
-			char *dest;
-			if (asprintf(&dest, "../entries/%s", entry->filename) ==
-			    -1)
-				return -ENOMEM;
-			hashmap_insert(entries_today, filename, dest);
-		}
+		pthread_mutex_unlock(&entries_mutex);
+		return 0;
 	}
 
-	hashmap_free_keys(keys, n_keys);
-	return 0;
+	return -ENOENT;
 }
 
 int
@@ -473,6 +603,7 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 	if (strcmp(path, "/") == 0) {
 		filler(buf, "entries", NULL, 0, 0);
 		filler(buf, "today", NULL, 0, 0);
+		filler(buf, "notes", NULL, 0, 0);
 	}
 	else if (strcmp(path, "/entries") == 0) {
 		size_t n_keys = 0;
@@ -490,7 +621,11 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 	}
 	else if (strcmp(path, "/today") == 0) {
 		size_t n_keys = 0;
-		buildTodayHashmap();
+		LOG("BUILDING TODAY HASHMAP");
+
+		build_today_hashmap();
+
+		LOG("GETTING KEYS");
 		char **keys = hashmap_get_keys(entries_today, &n_keys);
 		if (!keys) {
 			return -ENOMEM;
@@ -501,6 +636,23 @@ journal_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
 
 		hashmap_free_keys(keys, n_keys);
 	}
+	else if (strcmp(path, "/notes") == 0) {
+		size_t n_keys = 0;
+		LOG("BUILDING NOTES HASHMAP");
+
+		build_notes_hashmap();
+
+		LOG("GETTING KEYS");
+		char **keys = hashmap_get_keys(entries_notes, &n_keys);
+		if (!keys) {
+			return -ENOMEM;
+		}
+		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;
 }
@@ -575,6 +727,7 @@ parse_entry_content(journal_entry *entry, const char *buf, size_t size)
 	char *copy = strndup(buf, size);
 
 	char *description = NULL;
+
 	char *sep = strstr(copy, "\n---\n");
 
 	// Malformed input
@@ -797,10 +950,8 @@ create_new_unique_ics_uid()
 }
 
 icalcomponent *
-create_vjournal_entry(time_t created_at)
+create_vjournal_entry(char *uid)
 {
-	struct icaltimetype created_at_ical = icaltime_from_timet_with_zone(
-	    created_at, 0, icaltimezone_get_utc_timezone());
 
 	icalcomponent *calendar = icalcomponent_new_vcalendar();
 
@@ -811,11 +962,11 @@ create_vjournal_entry(time_t created_at)
 	// Step 2: Create a VJOURNAL entry
 	icalcomponent *journal = icalcomponent_new_vjournal();
 
-	icalcomponent_add_property(
-	    journal, icalproperty_new_uid(create_new_unique_ics_uid()));
+	icalcomponent_add_property(journal, icalproperty_new_uid(uid));
 
-	icalcomponent_add_property(journal,
-				   icalproperty_new_dtstamp(created_at_ical));
+	icalcomponent_add_property(
+	    journal, icalproperty_new_dtstamp(icaltime_current_time_with_zone(
+			 icaltimezone_get_utc_timezone())));
 	icalcomponent_add_property(journal, icalproperty_new_summary(""));
 	icalcomponent_add_property(journal, icalproperty_new_description(""));
 
@@ -824,26 +975,36 @@ create_vjournal_entry(time_t created_at)
 	return calendar;
 }
 
+void
+remove_file_extension(char *path)
+{
+	char *last_dot = strrchr(path, '.');
+	char *last_slash = strrchr(path, '/');
+
+	// Only remove the extension if dot is after last slash
+	if (last_dot && (!last_slash || last_dot > last_slash)) {
+		*last_dot = '\0';
+	}
+}
+
 int
 journal_create(const char *filename, mode_t mode, struct fuse_file_info *info)
 {
 	pthread_mutex_lock(&entries_mutex);
 	LOG("CREATE %s", filename);
 
-	// Strip path, validate just the new basename
-	const char *new_basename = strrchr(filename, '/');
-	new_basename++;
-	time_t created_at = timestamp_from_filename(new_basename);
-
-	if (created_at == INVALID_TIME) {
-		LOG("Invalid filename format: %s", new_basename);
-		pthread_mutex_unlock(&entries_mutex);
+	const char *entry_prefix = "/entries/";
+	if (strncmp(filename, entry_prefix, strlen(entry_prefix)) != 0) {
 		return -EINVAL;
 	}
+	char *new_basename = strrchr(filename, '/');
+	new_basename++;
+	char *without_extension = strdup(new_basename);
+	remove_file_extension(without_extension);
 
-	LOG("Got created at");
 	journal_entry *new_entry = xcalloc(1, sizeof(journal_entry));
-	icalcomponent *vjournal_component = create_vjournal_entry(created_at);
+	icalcomponent *vjournal_component =
+	    create_vjournal_entry(without_extension);
 
 	new_entry->component = vjournal_component;
 	new_entry->filename = strdup(new_basename);
@@ -1073,6 +1234,7 @@ main(int argc, char *argv[])
 	hashmap_free(entries_fuse);
 	hashmap_free(entries_original_ics);
 	hashmap_free(entries_today);
+	hashmap_free(entries_notes);
 	LOG("Hashmap freed");
 
 	regfree(&regex_fuse_file);
diff --git a/shell.nix b/shell.nix
@@ -15,5 +15,6 @@ pkgs.mkShell {
     valgrind
     libuuid
     libical
+    scdoc
   ];
 }