agendafs
A filesystem for your calendar.
git clone git://mccd.space/agendafscommit 4ab4a8d3921063c124b2c40179f3139d5e76d2d4
parent 1052e9696906fdcb40eb8bf92caecad178643b82
Author: Marc Coquand <marc@coquand.email>
Date: Tue, 10 Jun 2025 12:09:43 +0100
Add notes support
Diffstat:
| M | README.md | | | 32 | ++++++++++++++++++++++++++++++-- |
| M | main.c | | | 266 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
| M | shell.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(®ex_fuse_file);
diff --git a/shell.nix b/shell.nix
@@ -15,5 +15,6 @@ pkgs.mkShell {
valgrind
libuuid
libical
+ scdoc
];
}