Org Roam and Nyxt: taking Zettelkasten notes from the web
Too long; didn't read
Org Roam V2 came out recently and it is time to take notes directly from the web (via Nyxt).
The problem
Sometimes I get an idea while I browse the web. Earlier on I discovered how much I like managing notes with the Zettelkasten method. So given I got this idea while using a web browser, I would need to fire my Emacs, copy the link, then copy the quote, finally write my idea about what I read. Each time, I would lose the flow of my reading. Org Roam people would say I could set up org-protocol for that, but, again, that is a bit cumbersome for me.
How difficult is to take an Org Roam note directly from my browser?
And there is a solution
Nyxt is the solution really! A browser that feels as malleable as Emacs. In previous blogs I have shown how to make Nyxt speak to Emacs and even how to capture Org notes. If that is possible, we should be able to capture an Org Roam note as well.
Nyxt comes with a nice prompt system to request text from the user. So I decided to make a function to ask for the title of the note and its contents. My aim is to take a note on the fly, so I expect to write no more than a line. That is why the prompt space should be enough to draft out what I have in mind.
Well let me show you how this looks before we get into the implementation details.
This is the snippet you can add to your Nyxt configuration that makes it happen.
(define-command-global org-roam-capture ()
"Org-roam-capture current page."
(let ((quote (%copy))
(title (prompt
:input (title (current-buffer))
:prompt "Title of note:"
:sources (list (make-instance 'prompter:raw-source))))
(text (prompt
:input ""
:prompt "Note to take:"
:sources (list (make-instance 'prompter:raw-source)))))
(eval-in-emacs
`(let ((file (on/make-filepath ,(car title) (current-time))))
(on/insert-org-roam-file
file
,(car title)
nil
(list ,(render-url (url (current-buffer))))
,(car text)
,quote)
(find-file file)
(org-id-get-create)))
(echo "Org Roam Note stored!")))
You can see how we used the prompts. The prompt returns a list of
values, that is why we need (car title). If you select some text,
that will be inserted as well in the new note ((%copy)). I also open
the note in Emacs as a reminder that I need to rework it later
((find-file file)).
Actually, you can see that we use two functions here
on/insert-org-roam-file and on/make-filepath. These need to be
available in your Emacs.
This is how they look.
(defun on/slug-string (title) (let ((slug-trim-chars '(;; Combining Diacritical Marks https://www.unicode.org/charts/PDF/U0300.pdf
768 ; U+0300 COMBINING GRAVE ACCENT
769 ; U+0301 COMBINING ACUTE ACCENT
770 ; U+0302 COMBINING CIRCUMFLEX ACCENT
771 ; U+0303 COMBINING TILDE
772 ; U+0304 COMBINING MACRON
774 ; U+0306 COMBINING BREVE
775 ; U+0307 COMBINING DOT ABOVE
776 ; U+0308 COMBINING DIAERESIS
777 ; U+0309 COMBINING HOOK ABOVE
778 ; U+030A COMBINING RING ABOVE
780 ; U+030C COMBINING CARON
795 ; U+031B COMBINING HORN
803 ; U+0323 COMBINING DOT BELOW
804 ; U+0324 COMBINING DIAERESIS BELOW
805 ; U+0325 COMBINING RING BELOW
807 ; U+0327 COMBINING CEDILLA
813 ; U+032D COMBINING CIRCUMFLEX ACCENT BELOW
814 ; U+032E COMBINING BREVE BELOW
816 ; U+0330 COMBINING TILDE BELOW
817 ; U+0331 COMBINING MACRON BELOW
)))
(cl-flet* ((nonspacing-mark-p (char)
(memq char slug-trim-chars))
(strip-nonspacing-marks (s)
(ucs-normalize-NFC-string
(apply #'string (seq-remove #'nonspacing-mark-p
(ucs-normalize-NFD-string s)))))
(cl-replace (title pair)
(replace-regexp-in-string (car pair) (cdr pair) title)))
(let* ((pairs `(("[^[:alnum:][:digit:]]" . "_") ;; convert anything not alphanumeric
("__*" . "_") ;; remove sequential underscores
("^_" . "") ;; remove starting underscore
("_$" . ""))) ;; remove ending underscore
(slug (-reduce-from #'cl-replace (strip-nonspacing-marks title) pairs)))
(downcase slug)))))
(defun on/make-filepath (title now &optional zone)
"Make filename from note TITLE and NOW time (assumed in the current time ZONE)."
(concat
org-roam-directory
(format-time-string "%Y%m%d%H%M%S_" now (or zone (current-time-zone)))
(s-truncate 70 (on/slug-string title) "")
".org"))
(defun on/insert-org-roam-file (file-path title &optional links sources text quote)
"Insert org roam file in FILE-PATH with TITLE, LINKS, SOURCES, TEXT, QUOTE."
(with-temp-file file-path
(insert
"* " title "\n"
"\n"
"- tags :: " (--reduce (concat acc ", " it) links) "\n"
(if sources (concat "- source :: " (--reduce (concat acc ", " it) sources) "\n") "")
"\n"
(if text text "")
"\n"
"\n"
(if quote
(concat "#+begin_src text \n"
quote "\n"
"#+end_src")
"")))
(with-file file-path
(org-id-get-create)
(save-buffer)))
These functions use my note template. A note with my template looks like the following.
* some note. - tags :: ?? - source :: some-link Idea. #+begin_src text Some quote #+end_src
You may not like that, so please edit on/insert-org-roam-file as you
like (I attempted to go through org-roam-capture for this, but it
still does not seem program friendly). Note also that on/slug-string
is reusing the (latest at the time of writing) org-roam code to make
a file path.
And like that we have opened a world of note taking from our browser!
Conclusion
Here you go: note taking on the web! If you want a ready made solution for this, check out (and load) my hacks at emacs-with-nyxt. It will provide this command by default. Hope you will capture amazing ideas!
Happy note taking!