@ -36,6 +36,7 @@
;;;; Library Requires
( require 'org )
( require 'org-element )
( require 'org-capture )
( require 'ob-core ) ;for org-babel-parse-header-arguments
( require 'subr-x )
( require 'dash )
@ -69,12 +70,6 @@ All Org files, at any level of nesting, is considered part of the Org-roam."
:type 'directory
:group 'org-roam )
( defcustom org-roam-new-file-directory nil
" Path to where new Org-roam files are created.
If nil , default to the org-roam-directory ( preferred ) . "
:type 'directory
:group 'org-roam )
( defcustom org-roam-buffer-position 'right
" Position of `org-roam' buffer.
Valid values are
@ -109,26 +104,6 @@ If nil, always ask for filename."
:type 'boolean
:group 'org-roam )
( defcustom org-roam-graph-viewer ( executable-find " firefox " )
" Path to executable for viewing SVG. "
:type 'string
:group 'org-roam )
( defcustom org-roam-graphviz-executable ( executable-find " dot " )
" Path to graphviz executable. "
:type 'string
:group 'org-roam )
( defcustom org-roam-graph-max-title-length 100
" Maximum length of titles in Graphviz graph nodes. "
:type 'number
:group 'org-roam )
( defcustom org-roam-graph-node-shape " ellipse "
" Shape of Graphviz nodes. "
:type 'string
:group 'org-roam )
;;;; Dynamic variables
( defvar org-roam--current-buffer nil
" Currently displayed file in `org-roam' buffer. " )
@ -596,21 +571,8 @@ specified via the #+ROAM_ALIAS property."
( slug ( -reduce-from #' replace title pairs ) ) )
( s-downcase slug ) ) ) )
;;;; New file creation
( defvar org-roam-templates
( list ( list " default " ( list :file #' org-roam--file-name-timestamp-title
:content " #+TITLE: ${title} " ) ) )
" Templates to insert for new files in org-roam. " )
( defun org-roam--file-name-timestamp-title ( title )
" Return a file name (without extension) for new files.
It uses TITLE and the current timestamp to form a unique title. "
( let ( ( timestamp ( format-time-string " %Y%m%d%H%M%S " ( current-time ) ) )
( slug ( org-roam--title-to-slug title ) ) )
( format " %s_%s " timestamp slug ) ) )
( defun org-roam--new-file-path ( id &optional absolute )
;;;; Org-roam capture
( defun org-roam--new-file-path ( id )
" The file path for a new Org-roam file, with identifier ID.
If ABSOLUTE, return an absolute file-path. Else, return a relative file-path. "
( let ( ( absolute-file-path ( file-truename
@ -618,44 +580,111 @@ If ABSOLUTE, return an absolute file-path. Else, return a relative file-path."
( if org-roam-encrypt-files
( concat id " .org.gpg " )
( concat id " .org " ) )
( or org-roam-new-file-directory
org-roam-directory ) ) ) ) )
( if absolute
absolute-file-path
( file-relative-name absolute-file-path
( file-truename org-roam-directory ) ) ) ) )
( defun org-roam--get-template ( &optional template-key )
" Return an Org-roam template. TEMPLATE-KEY is used to get a template. "
( unless org-roam-templates
( user-error " No templates defined " ) )
( if template-key
( cadr ( assoc template-key org-roam-templates ) )
( if ( = ( length org-roam-templates ) 1 )
( cadar org-roam-templates )
( cadr ( assoc ( completing-read " Template: " org-roam-templates )
org-roam-templates ) ) ) ) )
( defun org-roam--make-new-file ( &optional info )
( let ( ( template ( org-roam--get-template ( cdr ( assoc 'template info ) ) ) )
( title ( or ( cdr ( assoc 'title info ) )
( completing-read " Title: " nil ) ) )
file-name-fn file-path )
( fset 'file-name-fn ( plist-get template :file ) )
( setq file-path ( org-roam--new-file-path ( file-name-fn title ) t ) )
( push ( cons 'slug ( org-roam--title-to-slug title ) ) info )
( unless ( file-exists-p file-path )
( org-roam--touch-file file-path )
( write-region
( s-format ( plist-get template :content )
'aget
info )
nil file-path nil ) )
( org-roam--db-update-file file-path )
org-roam-directory ) ) ) )
absolute-file-path ) )
( defvar org-roam--capture-file-name-default " %<%Y%m%d%H%M%S> "
" The default file name format for org-roam templates. " )
( defvar org-roam--capture-header-default " #+TITLE: ${title} \n "
" The default file name format for org-roam templates. " )
( defvar org-roam--capture-file-path nil
" The file path for the Org-roam capture. This variable is set
during the Org-roam capture process. " )
( defvar org-roam--capture-info nil
" An alist of additional information passed to the org-roam
template. This variable is populated dynamically, and is only
non-nil during the org-roam capture process. " )
( defvar org-roam--capture-context nil
" A cons cell containing the context (search term) to get the
exact point in a file. This variable is populated dynamically,
and is only active during an org-roam capture process.
E.g. ( 'title . \"New Title\" ) " )
( defvar org-roam-capture-templates
' ( ( " d " " default " plain ( function org-roam--capture-get-point )
" %? "
:file-name " %<%Y%m%d%H%M%S>-${slug} "
:head " #+TITLE: ${title} \n "
:unnarrowed t ) )
" Capture templates for org-roam. " )
( defun org-roam--fill-template ( str &optional info )
" Return a file name from template STR. "
( -> str
( s-format ( lambda ( key )
( or ( s--aget info key )
( completing-read ( format " %s: " key ) nil ) ) ) nil )
( org-capture-fill-template ) ) )
( defun org-roam--capture-new-file ( )
" Creates a new file, by reading the file-name attribute of the
currently active org-roam template. Returns the path to the new file. "
( let* ( ( name-templ ( or ( org-capture-get :file-name )
org-roam--capture-file-name-default ) )
( new-id ( s-trim ( org-roam--fill-template
name-templ
org-roam--capture-info ) ) )
( file-path ( org-roam--new-file-path new-id ) ) )
( org-roam--touch-file file-path )
( write-region
( org-roam--fill-template ( or ( org-capture-get :head )
org-roam--capture-header-default )
org-roam--capture-info )
nil file-path nil )
( sleep-for 0.2 ) ;; Hack: expand-file-name stringp nil error sporadically otherwise
file-path ) )
( defun org-roam--capture-get-point ( )
" Returns exact point to file for org-capture-template.
The file to use is dependent on the context:
If the search is via title, it is assumed that the file does not
yet exist, and org-roam will attempt to create new file.
If the search is via ref, it is matched against the Org-roam database.
If there is no file with that ref, a file with that ref is created. "
( pcase org-roam--capture-context
( 'title
( let ( ( file-path ( org-roam--capture-new-file ) ) )
( setq org-roam--capture-file-path file-path )
( set-buffer ( org-capture-target-buffer file-path ) )
( widen )
( goto-char ( point-max ) ) ) )
( 'ref
( let* ( ( completions ( org-roam--get-ref-path-completions ) )
( ref ( cdr ( assoc 'ref org-roam--capture-info ) ) )
( file-path ( or ( cdr ( assoc ref completions ) )
( org-roam--capture-new-file ) ) ) )
( setq org-roam--capture-file-path file-path )
( set-buffer ( org-capture-target-buffer file-path ) )
( widen )
( goto-char ( point-max ) ) ) )
( _ ( error " Invalid org-roam-capture-context. " ) ) ) )
( defun org-roam-capture ( &optional goto keys )
" Create a new file using an Org-roam template, and returns the
path to the edited file. The templates are defined at
` org-roam-capture-templates '. "
( interactive " P " )
( let ( ( org-capture-templates org-roam-capture-templates )
file-path )
( when ( = ( length org-capture-templates ) 1 )
( setq keys ( caar org-capture-templates ) ) )
( org-capture goto keys )
( setq file-path org-roam--capture-file-path )
( setq org-roam--capture-file-path nil )
file-path ) )
;;; Interactive Commands
;;;; org-roam-insert
( defvar org-roam--capture-insert-point nil
" The point to jump to after the call to `org-roam-insert' . " )
( defun org-roam-insert ( prefix )
" Find an org-roam file, and insert a relative org link to it at point.
If PREFIX, downcase the title before insertion. "
@ -669,22 +698,44 @@ If PREFIX, downcase the title before insertion."
( completions ( org-roam--get-title-path-completions ) )
( title ( completing-read " File: " completions nil nil region-text ) )
( region-or-title ( or region-text title ) )
( absolute-file-path ( or ( cdr ( assoc title completions ) )
( org-roam--make-new-file ( list ( cons 'title title ) ) ) ) )
( target-file-path ( cdr ( assoc title completions ) ) )
( current-file-path ( -> ( or ( buffer-base-buffer )
( current-buffer ) )
( buffer-file-name )
( file-truename )
( file-name-directory ) ) ) )
( when region ;; Remove previously selected text.
( goto-char ( car region ) )
( delete-char ( - ( cdr region ) ( car region ) ) ) )
( insert ( format " [[%s][%s]] "
( concat " file: " ( file-relative-name absolute-file-path
current-file-path ) )
( format org-roam-link-title-format ( if prefix
( downcase region-or-title )
region-or-title ) ) ) ) ) )
( file-name-directory ) ) )
( buf ( current-buffer ) )
( p ( point-marker ) ) )
( unless ( and target-file-path
( file-exists-p target-file-path ) )
( let* ( ( org-roam--capture-info ( list ( cons 'title title )
( cons 'slug ( org-roam--title-to-slug title ) ) ) )
( org-roam--capture-context 'title ) )
( setq target-file-path ( org-roam-capture ) ) ) )
( with-current-buffer buf
( when region ;; Remove previously selected text.
( delete-region ( car region ) ( cdr region ) ) )
( let ( ( link-location ( concat " file: " ( file-relative-name target-file-path current-file-path ) ) )
( description ( format org-roam-link-title-format ( if prefix
( downcase region-or-title )
region-or-title ) ) ) )
( goto-char p )
( insert ( format " [[%s][%s]] "
link-location
description ) )
( setq org-roam--capture-insert-point ( point ) ) ) ) ) )
( defun org-roam--capture-advance-point ( )
" Advances the point if it is updated.
We need this function because typically org-capture prevents the
point from being advanced, whereas when a link is inserted, the
point moves some characters forward. This is added as a hook to
` org-capture-after-finalize-hook '. "
( when org-roam--capture-insert-point
( goto-char org-roam--capture-insert-point )
( setq org-roam--capture-insert-point nil ) ) )
( add-hook 'org-capture-after-finalize-hook #' org-roam--capture-advance-point )
;;;; org-roam-find-file
( defun org-roam--get-title-path-completions ( )
@ -705,10 +756,14 @@ If PREFIX, downcase the title before insertion."
" Find and open an org-roam file. "
( interactive )
( let* ( ( completions ( org-roam--get-title-path-completions ) )
( title-or-slug ( completing-read " File: " completions ) )
( absolute-file-path ( or ( cdr ( assoc title-or-slug completions ) )
( org-roam--make-new-file ( list ( cons 'title title-or-slug ) ) ) ) ) )
( find-file absolute-file-path ) ) )
( title ( completing-read " File: " completions ) )
( file-path ( cdr ( assoc title completions ) ) ) )
( if file-path
( find-file file-path )
( let* ( ( org-roam--capture-info ( list ( cons 'title title )
( cons 'slug ( org-roam--title-to-slug title ) ) ) )
( org-roam--capture-context 'title ) )
( org-roam-capture ' ( 4 ) ) ) ) ) )
;;;; org-roam-find-ref
( defun org-roam--get-ref-path-completions ( )
@ -724,11 +779,8 @@ INFO is an alist containing additional information."
( interactive )
( let* ( ( completions ( org-roam--get-ref-path-completions ) )
( ref ( or ( cdr ( assoc 'ref info ) )
( completing-read " Ref: " ( org-roam--get-ref-path-completions ) ) ) )
( file-path ( cdr ( assoc ref completions ) ) ) )
( if file-path
( find-file file-path )
( find-file ( org-roam--make-new-file info ) ) ) ) )
( completing-read " Ref: " ( org-roam--get-ref-path-completions ) nil t ) ) ) )
( find-file ( cdr ( assoc ref completions ) ) ) ) )
;;;; org-roam-switch-to-buffer
( defun org-roam--get-roam-buffers ( )
@ -756,10 +808,15 @@ INFO is an alist containing additional information."
;;;; Daily notes
( defun org-roam--file-for-time ( time )
" Create and find file for TIME. "
( let* ( ( org-roam-templates ( list ( list " daily " ( list :file ( lambda ( title ) title )
:content " #+TITLE: ${title} " ) ) ) ) )
( org-roam--make-new-file ( list ( cons 'title ( format-time-string " %Y-%m-%d " time ) )
( cons 'template " daily " ) ) ) ) )
( let* ( ( title ( format-time-string " %Y-%m-%d " time ) )
( org-roam-capture-templates ( list ( list " d " " daily " 'plain ( list 'function #' org-roam--capture-get-point )
" "
:immediate-finish t
:file-name " ${title} "
:head " #+TITLE: ${title} " ) ) )
( org-roam--capture-context 'title )
( org-roam--capture-info ( list ( cons 'title title ) ) ) )
( org-roam-capture ) ) )
( defun org-roam-today ( )
" Create and find file for today. "
@ -786,7 +843,6 @@ INFO is an alist containing additional information."
( let ( ( path ( org-roam--file-for-time time ) ) )
( org-roam--find-file path ) ) ) )
;;; The org-roam buffer
;;;; org-roam-link-face
( defface org-roam-link
@ -968,6 +1024,28 @@ Valid states are 'visible, 'exists and 'none."
( 'none ( org-roam--setup-buffer ) ) ) )
;;; The graphviz links graph
;;;; Options
( defcustom org-roam-graph-viewer ( executable-find " firefox " )
" Path to executable for viewing SVG. "
:type 'string
:group 'org-roam )
( defcustom org-roam-graphviz-executable ( executable-find " dot " )
" Path to graphviz executable. "
:type 'string
:group 'org-roam )
( defcustom org-roam-graph-max-title-length 100
" Maximum length of titles in Graphviz graph nodes. "
:type 'number
:group 'org-roam )
( defcustom org-roam-graph-node-shape " ellipse "
" Shape of Graphviz nodes. "
:type 'string
:group 'org-roam )
;;;; Functions
( defun org-roam--build-graph ( )
" Build the Graphviz string.
The Org-roam database titles table is read, to obtain the list of titles.