This document contains all the source code to a set of functions for emacs
which aim to extend the deft
package and turn it into a (very very) basic Zettelkasten note-taking system.
A concice presentation about the package can be found in Introducing Zetteldeft.
Check out the Github repository to get the source. Read on for an introduction and some documentation.
Latest additions:
- 21 Sep 2022: Add automated list links via Org Dynamic Blocks. See documentation below.
- 31 Mar 2022: Add
-full-search-
versions of finding & linking functions, thanks to localauthor - 19 Aug: Update
zetteldeft-browse
to handle more keys - 01 Aug: Automated list of links with
zetteldeft-insert-list-links-block
- 13 Jul: Where backlinks go is now customizable with
zetteldeft-backlink-location-function
- 19 May: Add
zetteldeft-extract-region-to-note
, thanks to Sodaware - 13 May: Introduce & customize
zetteldeft--insert-link
- 11 Mar: Add
zetteldeft-tag-insert
, thanks to puzan
Older changes are mentioned in the changelog below.
This is my feeble attempt at recreating a Zettelkasten environment by extending the excellent deft
package in emacs
.[fn:deft]
I call it zetteldeft.
It is inspired by the The Archive app. For this and more on the Zettelkasten way of taking notes, see zettelkasten.de. They have a forum for discussion on both software and the specifics of the Zettelkasten philosophy.
The code that follows is created and maintained for my personal use, shared here in hope that it can benefit others as well. I’d be happy to learn how you use it and expand upon it.
It is very much WIP and I’m fairly new to elisp
, so it might contain some stupid code.
Anyway, here we go.
[fn:deft] For those not yet familiar: deft is a note manager within emacs
, for easily searching and retrieving plain text notes.
It is inspired by the popular Notational Velocity.
Check out jblevins.org/projects/deft/ and notational.net.
If you use MELPA & use-package
, installing is easy.
Simply add
(use-package zetteldeft
:after deft
:config
(zetteldeft-set-classic-keybindings))
to your init.el
.
The zetteldeft-set-classic-keybindings
sets up some defaults as per section #suggested-kb.
If you prefer evil
style bindings, leave out this function call and take a look at #kb-general.
For the best experience, configure deft
as suggested in section #suggested-deft.
An installation of avy
is required, as it is used for some functions that make following links easier.
Installing and enabling zetteldeft
with the default configuration is straightforward with spacemacs
:
- Add the
deft
layer to your.spacemacs
file and enablezetteldeft
support. Locatedotspacemacs-configuration-layers
in your.spacemacs
and add the variables as suggested below. This should install bothdeft
andzetteldeft
and load them in correct order, and set default keybindings as shared in section #kb-spacemacs.(setq-default dotspacemacs-configuration-layers '((deft :variables deft-zetteldeft t)))
- You can further configure
deft
according to section #suggested-deft. Do this thespacemacs
way, i.e. indotspacemacs/user-config
. Simply add(setq ...)
there.
Installing zetteldeft
for doom-emacs
is pretty simple.
In doom
, you can navigate to your config files by typing SPC f p
.
- Within your
packages.el
file, add this line:(package! zetteldeft)
- Within
init.el
uncommentdeft
(found under:ui
) by deleting the semicolons in front of it.avy
andace-window
are included by default withdoom
, so no need to install them. - Refresh your
doom
session by typingSPC h r r
, or by restarting emacs.
This package requires:
deft
, obviouslyavy
to jump & searchace-window
to follow a link in a window of choice
From the Github repository, either
- download the
zetteldeft.el
file, - download the
org-file
andorg-tangle
it yourself. It should contain everything.
Whichever way you go, load up the package by adding the package to your load path and requiring:
(add-to-list 'load-path "~/path/to/folder/")
(require 'zetteldeft)
and you’re good to go!
Well, not quite.
First you must read on about the basics of zetteldeft
.
You’ll also want some keybindings. Check out the suggested setup below.
Notes reside in the deft-directory
.
Notes are written in org-mode
syntax (although most functions should work in markdown
as well).
The filename of a note starts with a unique id based on the time and the date, for example: 2018-07-09-2115 This is a note.org
.
This unique id can be used to link notes together.
A link consists of the §
character followed by the id.
For example: §2018-07-09-2115
should link to the file above.
A link can appear anywhere in the text.
See below for advanced information about IDs and links.
When searching deft
with the id as a filter, you’ll find both the original note (with the id in its name) and all the notes that link to this note (with the id in its body). Do so with zetteldeft-search-current-id
and zetteldeft-avy-link-search
respectively
Notes can contain tags in plain text: words prepended with a #
.
This is a tag: #tag
.
Tags make it easy to retrieve notes. They can appear anywhere in the note, but I’d suggest putting them somewhere at the top.
Create a note with zetteldeft-new-file
and provide a name.
To insert links to other notes, either
- enter their links manually,
- use
zetteldeft-find-file-id-insert
and select a file from the list,
With zetteldeft-find-file-full-title-insert
, you guessed it, the note’s title is included as well.
To easily branch out from the current note (i.e. create a new one and link to it in one go), use zetteldeft-new-file-and-link
.
To search for a tag or anything else under cursor, use zetteldeft-search-at-point
.
Combined with the power of avy
to jump to any character on screen, use these to jump and search in one go: zetteldeft-avy-link-search
and zetteldeft-avy-tag-search
.
To open the note behind a link, use zetteldeft-follow-link
.
Want more functionality? How about showing a list of tags or gathering notes with a certain search string? Or maybe a graph visualizing how notes are linked?
Still hungry? I’m welcoming both contributions and suggestions. Feel free to submit comments or pull requests on Github.
While there are many, these should be enough to get you started. Here is an overview.
Note that the package itself does not define any keybindings.
You’ll need to set up those yourself, but defaults are suggested at the end of this document.
Keybindings in the overview below are preceded by C-c d
or SPC d
, depending on the setup.
Function | Description | Keybinding |
---|---|---|
zetteldeft-new-file | Create new note and open | d n |
zetteldeft-new-file-and-link | Create new note and insert link | d N |
zetteldeft-find-file-id-insert | Pick a note and insert a link | d i |
zetteldeft-follow-link | Follow a link | d f |
zetteldeft-avy-link-search | Select and search a link’s ID | d l |
zetteldeft-avy-tag-search | Select a tag and search for it | d t |
zetteldeft-search-at-point | Search for thing at point | d s |
zetteldeft-search-current-id | Search for id of current file | d c |
Read on, dear reader, for all of this and much more.
The required preamble and some other initial settings. To know how this package works, please skip right past this to the next section.
Some declaration.
;;; zetteldeft.el --- Turn deft into a zettelkasten system -*- lexical-binding: t -*-
;; Copyright (C) 2018-2021 EFLS
;; Author: EFLS <Elias Storms>
;; URL: https://efls.github.io/zetteldeft/
;; Keywords: deft zettelkasten zetteldeft wp files
;; Version: 0.3
;; Package-Requires: ((emacs "25.1") (deft "0.8") (ace-window "0.7.0"))
;; This file is not part of Emacs
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; Zetteldeft is an extension of the deft package for Emacs.
;; It generates unique IDs to create stable links between notes, which
;; allows the user to make an interconnected system of notes.
;; Zetteldeft uses deft to find and follow links to notes.
;; For more information, see zetteldeft.org
;; or https://efls.github.io/zetteldeft
;; Note: this file is tangled from zetteldeft.org.
;; The .org contains documentation and notes on usage of the package.
;;; Code:
deft
is required, obviously, and avy
is needed for some utility functions.
thingatpt
is needed to easily search & jump.
ace-window
is used to follow links in another window.
(require 'deft)
(unless (require 'avy nil 'no-error)
(user-error "Avy not installed, required for zetteldeft-avy-* functions"))
(require 'thingatpt)
(require 'ace-window)
(require 'seq)
Since February 2019, the avy
API changed and avy--generic-jump
is replaced by avy-jump
.
Unfortunately, this change doesn’t seem to be indicated with a specific version number.
So let’s check whether that function is available, and show a message when it’s not.
(declare-function avy-jump "avy")
(unless (fboundp 'avy-jump)
(display-warning 'zetteldeft
"Function `avy-jump' not available. Please update `avy'"))
For easy but minor customization options.
(defgroup zetteldeft nil
"A zettelkasten on top of deft."
:group 'deft
:link '(url-link "https://efls.github.io/zetteldeft"))
In this section:
Search the thing at point.
Based on snippet suggested by saf-dmitry
on deft’s Github.
;;;###autoload
(defun zetteldeft-search-at-point ()
"Search via `deft' with `thing-at-point' as filter.
Thing can be a double-bracketed link, a hashtag, or a word."
(interactive)
(let ((string (zetteldeft--get-thing-at-point)))
(if string
(zetteldeft--search-global string t)
(user-error "No search term at point"))))
Deft search on the id of the current file.
This function is useful to easily see which notes link to the current file.
Result is not opened automatically.
Steps:
- Get the filename from the current buffer.
- Lift the ID from it.
- Search with resulting string.
;;;###autoload
(defun zetteldeft-search-current-id ()
"Search deft with the id of the current file as filter.
Open if there is only one result."
(interactive)
(zetteldeft--search-global
(zetteldeft--current-id) t))
Returns the thing at point as string.
Tries to get, in the following order:
- links between
[[
- hashtags according to
zetteldeft-tag-regex
- links according to the
zetteldeft-link-indicator
andzetteldeft-id-regex
- words
Based on snippet suggested by saf-dmitry
on deft’s Github.
(defun zetteldeft--get-thing-at-point ()
"Return the thing at point.
This can be
- a link: a string between [[ brackets ]],
- a tag matching `zetteldeft-tag-regex',
- a link matching `zetteldeft-link-indicator',
`zetteldeft-id-regex' and `zetteldeft-link-suffix',
- or a word."
(let* ((link-brackets-re "\\[\\[\\([^]]+\\)\\]\\]")
(link-id-re (zetteldeft--link-regex))
(htag-re zetteldeft-tag-regex))
(cond
((thing-at-point-looking-at link-brackets-re)
(match-string-no-properties 1))
((thing-at-point-looking-at link-id-re)
(match-string-no-properties 0))
((thing-at-point-looking-at htag-re)
(match-string-no-properties 0))
(t (thing-at-point 'word t)))))
Search with deft for given string. If there is only one result, that file is opened, unless additional argument is true.
Based on snippet suggested by saf-dmitry
on deft’s Github.
(defun zetteldeft--search-global (str &optional dntOpn)
"Search deft with STR as filter.
If there is only one result, open that file (unless DNTOPN is true)."
;; Sanitize the filter string
(setq str (replace-regexp-in-string "[[:space:]\n]+" " " str))
;; Switch to Deft window if buffer is currently visible
(when (deft-buffer-visible-p)
(select-window (deft-buffer-visible-p)))
;; Call deft search on the filter string
(let ((deft-incremental-search t))
(deft)
(deft-filter str t))
;; If there is a single match, open the file
(unless dntOpn
(when (eq (length deft-current-files) 1)
(deft-open-file (car deft-current-files)))))
When doing a global search, the Deft buffer is shown. To prevent that the Deft buffer is shown in multiple windows, first switch to the relevant window if the Deft buffer is already visible somewhere.
Deft search on filename. If there is only one result, open that file.
Incremental search is turned off, and the filter is set to filenames only.
(defun zetteldeft--search-filename (thisStr &optional otherWindow)
"Search for deft files with string THISSTR in filename.
Open if there is only one result (in another window if OTHERWINDOW is non-nil).
Return a message if no results are found."
;; Sanitize the filter string
(setq thisStr (replace-regexp-in-string "[[:space:]\n]+" " " thisStr))
;; Call deft search on the filter string
(let ((deft-filter-only-filenames t))
(deft-filter thisStr t))
;; If there is a single match, open the file
(cond
((eq (length deft-current-files) 1)
(deft-open-file (car deft-current-files) otherWindow))
((eq (length deft-current-files) 0)
(message "No notes found with %s in name." thisStr))))
Get a list of the files with given search string.
To fix: sorting of results.
The code searches for the given string and returns deft-current-files
.
(defun zetteldeft--get-file-list (srch)
"Return a list of files with the search item SRCH."
(let ((deft-current-sort-method 'title))
(deft-filter srch t)
deft-current-files))
This function allows the user to quickly search for a specific tag. It prompts the user for a tag via completion and launches a search.
This relies on zetteldeft--get-all-tags
, which is also used to generate the tag buffer.
;;;###autoload
(defun zetteldeft-search-tag ()
"Prompt interactively for Zetteldeft tag and launch Deft search"
(interactive)
(let* ((tags (zetteldeft--get-all-sorted-tags))
(search-term (completing-read "Tag to search for: " tags)))
(zetteldeft--search-global search-term t)))
As zetteldeft--get-all-tags
parses the full set of files, this function might lag slightly on very big sets of notes.
In zetteldeft, the concepts of “links” and “IDs” have related meanings, but in the documentation they are not synonyms.
An ID refers to the unique, generated string included in the filename, identifying each note.
By default, IDs are numeric and time-based.
For example: 2019-01-20-1433
.
These are generated by zetteldeft-generate-id
.
(To customize how IDs are generated, take a look at the variable zetteldeft-custom-id-function
.)
A link is an ID prepended by a character to easily identify it as a link.
For example: §2019-01-20-1433
.
This identifying character can be changed by setting the zetteldeft-link-indicator
variable and is §
by default.
Set it to an empty string (i.e. ""
) to disable the indicator.
See also zetteldeft-link-suffix
(which is empty by default) if you prefer a closure to the links.
Before the customization, let’s declare a helper function that adds a font-lock
keyword for org-mode
specifically.
The function below – courtesy of bymoz089 – is called when zetteldeft-link-indicator
or zetteldeft-id-regex
are customized.
The function
- Removes existing keywords from the font-lock setup
- Sets the variable and value
- Adds them as font-lock keywords
To highlight links, zetteldeft-id-regex
prepended by zetteldeft-link-indicator
.
(defun zetteldeft--id-font-lock-setup (var val)
"Add font-lock highlighting for zetteldeft links.
Called when `zetteldeft-link-indicator' or
`zetteldeft-id-regex' are customized."
(when (and (boundp 'zetteldeft-link-indicator)
(boundp 'zetteldeft-id-regex)
(boundp 'zetteldeft-link-suffix))
(font-lock-remove-keywords 'org-mode
`((,(concat zetteldeft-link-indicator
zetteldeft-id-regex
zetteldeft-link-suffix)
. font-lock-warning-face))))
(set-default var val)
(when (and (boundp 'zetteldeft-id-regex)
(boundp 'zetteldeft-link-indicator)
(boundp 'zetteldeft-link-suffix))
(font-lock-add-keywords 'org-mode
`((,(concat zetteldeft-link-indicator
zetteldeft-id-regex
zetteldeft-link-suffix)
. font-lock-warning-face)))))
Note that highlighting is not working in org comments.
Format used to generate time-based IDs.
See documentation of format-time-string
for more info on possible placeholders.
If you customize this value, make sure to edit the zetteldeft-id-regex
as well, so that the IDs can be found by other functions.
(defcustom zetteldeft-id-format "%Y-%m-%d-%H%M"
"Format used when generating time-based zetteldeft IDs.
Be warned: the regexp to find IDs is set separately.
If you change this value, set `zetteldeft-id-regex' so that
the IDs can be found.
Check the documentation of the `format-time-string'
function to see which placeholders can be used."
:type 'string
:group 'zetteldeft)
Another popular option would be to set this value to "%Y%m%d%H%M"
, so that a similar ID is generated without any dashes.
See below for a corresponding regular expression.
Note that the default setup means that IDs are not unique when you make multiple notes per minute.
If you plan to do so, extend the zetteldeft-id-format
and add %S
to include seconds.
These IDs should still be recognized by the default zetteldeft-id-regex
below.
While we’re at it, lets tell deft
to use this format when creating new files.
For good measure: I advise creating new notes in the zetteldeft
system with zetteldeft-new-file
or zetteldeft-new-file-and-link
as defined below, rather than through deft
itself.
(setq deft-new-file-format zetteldeft-id-format)
Next up, a function to generate an ID string.
By default, the above time-based format will be used, unless a custom ID generator function is specified: to override the way an ID is generated, point zetteldeft-custom-id-function
to a custom function.
The above function performs a rudimentory check to ensure that the generated ID is indeed available (i.e., not yet used in a filename). The current response to such a duplicate is to throw an error. With the default, time-based IDs, this happens when notes are created in quick succession.
(defun zetteldeft-generate-id (title &optional filename)
"Generate and return a Zetteldeft ID.
The ID is created using `zetteldeft-id-format', unless
`zetteldeft-custom-id-function' is bound to a function, in which case
that function is used and TITLE and FILENAME are passed to it."
(let ((id
(if-let ((f zetteldeft-custom-id-function))
(funcall f title filename)
(format-time-string zetteldeft-id-format))))
(if (zetteldeft--id-available-p id)
id
(error "Generated ID %s is not unique." id))))
The check whether an ID is available is performed by a separate function.
(defun zetteldeft--id-available-p (str)
"Return t only if provided string STR is unique among Zetteldeft filenames."
(let ((deft-filter-only-filenames t))
(deft-filter str t))
(eq 0 (length deft-current-files)))
For full control over the output of zetteldeft-generate-id
, you can set the variable zetteldeft-custom-id-function
to a function of your choice.
When left nil
, default time-based IDs are generated.
(defcustom zetteldeft-custom-id-function nil
"User-defined function to generate an ID.
The specified function must accept arguments for note `TITLE'
and &optional `FILENAME'. The returned ID must be a string."
:type 'function
:group 'zetteldeft)
The function indicated by zetteldeft-custom-id-function
must accept arguments for title
and optional filename
.
The second argument will be passed only when a new ID is being generated for an existing note that does not already have an ID (for example, one created outside of zetteldeft).
The function may use or ignore its argument values as desired.
Let’s look at an example. Below is a custom ID function that generates an ID based on the current time for new notes, and based on the file’s last-updated time for existing notes not yet having an ID:
;; Hypothetical custom function defined in init.el
(defun my-time-based-id-example (title &optional filename)
"Generate an ID using a note's last-updated time if available"
(if filename
(let ((last-updated (file-attribute-modification-time
(file-attributes filename))))
(format-time-string zetteldeft-id-format last-updated))
(format-time-string zetteldeft-id-format)))
;; Tell zetteldeft to use the custom ID function
(setq zetteldeft-custom-id-function #'my-time-based-id-example)
Custom ID functions can produce IDs in any format, not just time-based ones. The following (non-working) example code illustrates this:
;; Hypothetical custom function defined in init.el
(defun my-arbitrary-id-example (title &optional filename)
"Generate an ID exactly the way I like it"
(if filename
(cryptographic-hash filename)
(random-anagram title)))
;; Tell zetteldeft to use the custom ID function
(setq zetteldeft-custom-id-function #'my-arbitrary-id-example)
If you set zetteldeft-custom-id-function
, be sure that the values your function generates are matched by zetteldeft-id-regex
, so that the IDs can be found by Zetteldeft.
The regular expression used to search for zetteldeft IDs as set in zetteldeft-id-format
.
The default regex dictates that a zetteldeft ID should consist of:
- a series of exactly 4 numbers
- followed by exactly 3 sets of a dash and two or more numbers
When customized, the function zetteldeft--id-font-lock-setup
is so that the zetteldeft id is fontified correctly.
(defcustom zetteldeft-id-regex "[0-9]\\{4\\}\\(-[0-9]\\{2,\\}\\)\\{3\\}"
"The regular expression used to search for zetteldeft IDs.
Set it so that it matches strings generated with
`zetteldeft-id-format'."
:type 'string
:group 'zetteldeft
:set 'zetteldeft--id-font-lock-setup)
If you use a "%Y%m%d%H%M"
format for note naming, you might want to set the regular expression to "20[0-9]\\{10\\}"
so that it matches any string starting with 20
followed by 10 other digits.
To make it easier to distinguish links to zetteldeft notes, the ID can be prepended with a symbol.
By default, this is set to §
, but it can be changed (or nil).
(defcustom zetteldeft-link-indicator "§"
"String to indicate zetteldeft links.
String prepended to IDs to easily identify them as links to zetteldeft notes.
This variable should be a string containing only one character."
:type 'string
:group 'zetteldeft
:set 'zetteldeft--id-font-lock-setup)
For further customizability, let’s also introduce a suffix. This will be appended to links.
By default, this is set to an empty string.
(defcustom zetteldeft-link-suffix ""
"String to append to zetteldeft links.
To disable, set to empty string rather than to nil."
:type 'string
:group 'zetteldeft
:set 'zetteldeft--id-font-lock-setup)
Users who prefer Markdown notes and interoperability with The Archive, might want to set this to ]]
, and the zetteldeft-link-indicator
to [[
.
To match Zetteldeft links, we need to concatenate the link indicator, the ID regex, and link suffix.
The following function returns such a string.
(defun zetteldeft--link-regex ()
"Return regex for a Zetteldeft link.
Concat link indicator, id-regex, and link suffix."
(concat zetteldeft-link-indicator
zetteldeft-id-regex
zetteldeft-link-suffix))
Return the zetteldeft ID from any string.
Searches with a temporary buffer, from the end of the string backwards (hence the -1
argument), which implies that the last zetteldeft string is returned.
(defun zetteldeft--lift-id (str)
"Extract zetteldeft ID from STR.
This is done with the regular expression stored in
`zetteldeft-id-regex'."
(with-temp-buffer
(insert str)
(when (re-search-forward zetteldeft-id-regex nil t -1)
(match-string 0))))
Or are there better ways than working with-temp-buffer
?
Here is a little test.
(zetteldeft--lift-id "2018-11-09-1934-12 Some text (1989) - testing (2000 p. 12-25)")
2018-11-09-1934
Inserts a link to a note. Requires an ID as argument and an optional title.
To format the inserted link, customize the zetteldeft-insert-link-function
variable.
(defun zetteldeft--insert-link (id &optional title)
"Insert a link to Zetteldeft note ID.
If TITLE is included, use it as link text. To customize how inserted
links are formatted, change the `zetteldeft-insert-link-function'
variable."
(interactive)
(funcall zetteldeft-insert-link-function id title))
This variable points to the function used to insert links. It can be customized to use a different link format.
To customize beyond the options provided below, point this variable to a custom function. The custom function should take two parameters:
- a required
id
: the note identifier - an optional
title
: used as descriptive link text
Note that for the best results, link descriptions should include a zetteldeft-link-indicator
, so that the avy
functions for jumping to links will recognize the links provided.
(defcustom zetteldeft-insert-link-function
#'zetteldeft-insert-link-zd-style
"The function to use when inserting note links.
Use either
- `zetteldeft-insert-link-zd-style' for Zetteldeft type links
- `zetteldeft-insert-link-org-style' for Org-mode zdlink: links
- A custom function that takes two arguments: an ID and an optional title."
:type 'function
:options '(zetteldeft-insert-link-zd-style
zetteldeft-insert-link-org-style)
:group 'zetteldeft)
The default function is zetteldeft-insert-link-zd-style
, which inserts:
- the
zetteldeft-link-indicator
(§
by default) - the ID of the destination note
- the
zetteldeft-link-suffix
(nil by default) - a space and the title (if provided)
(defun zetteldeft-insert-link-zd-style (id &optional title)
"Insert a Zetteldeft link to note with provided ID."
(insert zetteldeft-link-indicator
id
zetteldeft-link-suffix)
(when title (insert " " title)))
Alternatively, when using Org-mode style links, zetteldeft-insert-link-org-style
can be used.
It inserts a formatted Org-link with zdlink:
type.
When no title is provided, the link itself is used as a descriptor.
(defun zetteldeft-insert-link-org-style (id &optional title)
"Insert a Zetteldeft link in Org-mode format as zdlink: type."
(if title
(insert "[[zdlink:" id "][" title "]]")
(insert "[[zdlink:" id "]]")))
Open a Zetteldeft note from the minibuffer by prompting the user for a filename.
Based on deft-find-file
.
When there’s no match to the entered search string, propose to create a new note using the string as a title.
;;;###autoload
(defun zetteldeft-find-file (file)
"Open deft file FILE.
When no completing match, prompt user to create a new deft file using
input as the title."
(interactive
(list (completing-read "Deft find file: "
(deft-find-all-files-no-prefix))))
(let* ((dir (expand-file-name deft-directory)))
(unless (string-match (concat "^" dir) file)
(setq file (concat dir "/" file)))
(if (file-exists-p file)
(deft-open-file file)
(when (y-or-n-p (format
"Create new note with title \"%s\"?"
(file-name-base file)))
(zetteldeft-new-file (file-name-base file))))))
A Zettelkasten system has no fixed single hierarchy, but it is often convenient to maintain a base note, or a home note. Such a note can link to other notes that contain links related to a specific subtopic.
Opening up the home note should be quick and easy.
Let’s first store the availability to store an ID.
(defvar zetteldeft-home-id nil
"String with ID of home note, used by `zetteldeft-go-home'.")
A function to open up this note is trivial.
(defun zetteldeft-go-home ()
"Move to a designated home note.
Set `zetteldeft-home-id' to an ID string of your home note."
(interactive)
(if (stringp zetteldeft-home-id)
(zetteldeft-find-file
(zetteldeft--id-to-full-path zetteldeft-home-id))
(message "No home set. Provide a string to zetteldeft-home-id.")))
Select file from minibuffer and insert a link to it. Uses just the ID and doesn’t include a title description.
Based on deft-find-file
.
;;;###autoload
(defun zetteldeft-find-file-id-insert (file)
"Find deft file FILE and insert a link."
(interactive (list
(completing-read "File to insert id from: "
(deft-find-all-files-no-prefix))))
(zetteldeft--insert-link (zetteldeft--lift-id file)))
Adding a backlink to another note is a good way to create structure. A backlink is a single line with a prefix string followed by a link to a note and its title.
For such a backlink, we need a prefix.
(defcustom zetteldeft-backlink-prefix "# Backlink: "
"Prefix string included before a back link.
Formatted as `org-mode' comment by default."
:type 'string
:group 'zetteldeft)
We also need to figure out where backlinks should go. Let’s configure a customizable function to move to the location they should be put in.
(defcustom zetteldeft-backlink-location-function
#'zetteldeft-backlink-get-location
"Function to get location for new backlinks.
The function should return a position in the current buffer.")
This function should return a position in the current buffer that will then be called with goto-char
.
Now for the default function. It indicates backlinks should be included at one of the following, if they exist:
- below an existing backlink,
- below the tag line,
- below the title,
- at top of the file.
(defun zetteldeft-backlink-get-location ()
"Default function that returns where a backlink should be added.
This is the line below whichever is found first:
- existing backlink
- tag line
- title
- at top of file
"
(interactive)
(save-excursion
(goto-char (point-min))
(cond
((re-search-forward (regexp-quote zetteldeft-backlink-prefix) nil t)
(forward-line)
(point))
((re-search-forward (regexp-quote zetteldeft-tag-line-prefix) nil t)
(forward-line)
(newline)
(point))
((re-search-forward (regexp-quote zetteldeft-title-prefix) nil t)
(forward-line)
(newline)
(point))
(t (point-min)))))
We use this function to figure out where backlinks should go. The backlink appears on a new line.
;;;###autoload
(defun zetteldeft-backlink-add (file)
"Find deft file FILE and insert a backlink to it.
Finds the title line, and adds `backlink-prefix' with
ID and title on a new line."
(interactive (list
(completing-read "File to add backlink to: "
(deft-find-all-files-no-prefix))))
(save-excursion
(goto-char (funcall zetteldeft-backlink-location-function))
(insert zetteldeft-backlink-prefix)
(zetteldeft--insert-link
(zetteldeft--lift-id file)
(zetteldeft--lift-file-title (concat deft-directory file)))
(insert "\n"))
(message "Backlink added."))
Select file from minibuffer and insert a link to it, including the note’s title as description.
Based on deft-find-file
.
;;;###autoload
(defun zetteldeft-find-file-full-title-insert (file)
"Find deft file FILE and insert a link with title."
(interactive (list
(completing-read "File to insert full title from: "
(deft-find-all-files-no-prefix))))
(let ((id (zetteldeft--lift-id file)))
(zetteldeft--insert-link
id
(zetteldeft--id-to-title id))))
The previous functions allow to search & link based on filename. Sometimes its more convenient to do these actions based on full text search.
The functions below have been contributed by Github user localauthor.
We first need a helper function that returns a list of files based on the query from the minibuffer.
(defun zetteldeft--full-search (string)
"Return list of deft files with STRING in full body of file."
(let ((dir (expand-file-name deft-directory))
(result-files (zetteldeft--get-file-list string))
(this-file (buffer-file-name)))
(when this-file
(setq result-files (delete this-file result-files)))
(setq result-files
(mapcar
(lambda (f) (replace-regexp-in-string dir "" f))
result-files))
(completing-read (format "Search files containing \"%s\": " string)
result-files)))
This search function can then be used to insert links or open notes. We can use this to define three new functions: to open a file based on full text search, to insert an ID, or to insert the full title.
;;;###autoload
(defun zetteldeft-full-search-id-insert (string)
"Insert ID of file from list of files containing STRING."
(interactive (list (read-string "Search string: ")))
(zetteldeft-find-file-id-insert
(zetteldeft--full-search string)))
;;;###autoload
(defun zetteldeft-full-search-full-title-insert (string)
"Insert title and ID of file from list of files containing
STRING."
(interactive (list (read-string "Search string: ")))
(zetteldeft-find-file-full-title-insert
(zetteldeft--full-search string)))
;;;###autoload
(defun zetteldeft-full-search-find-file (string)
"Open file containing STRING."
(interactive (list (read-string "Search string: ")))
(zetteldeft-find-file (zetteldeft--full-search string)))
The string inserted between an ID and the rest of the filename.
By default, this is set to a single space, but it can be changed.
Use this when you use deft-file-naming-rules
to remove spaces from filenames.
(defcustom zetteldeft-id-filename-separator " "
"String to separate zetteldeft ID from filename."
:type 'string
:group 'zetteldeft)
Create new file with filename as zetteldeft-id-format
and a string.
Either provide a title as argument, or (when called interactively) enter one in the mini-buffer.
Additionally:
- generate an ID (unless one is provided as optional argument)
- insert a title in the newly created file, wrapped in
zetteldeft-title-prefix
andzetteldeft-title-suffix
- when
evil
is loaded, enter the insert state - add the new filename to the kill ring (if
zetteldeft-new-filename-to-kill-ring
is nil)
First, let’s make sure emacs
knows where to find evil-insert-state
.
(declare-function evil-insert-state "evil")
Next, the customization option to indicate whether the new filename should be added to the kill ring.
(defcustom zetteldeft-new-filename-to-kill-ring nil
"Add new filename to kill ring?"
:type 'boolean
:group 'zetteldeft)
Now for the function itself.
;;;###autoload
(defun zetteldeft-new-file (str &optional id)
"Create a new deft file.
The filename is a Zetteldeft ID, appended by STR. The ID will be
generated, unless ID is provided. A file title will be inserted in the
newly created file wrapped in `zetteldeft-title-prefix' and
`zetteldeft-title-suffix'. When `zetteldeft-new-filename-to-kill-ring'
is non-nil, the filename (without extension) is added to the kill
ring. When `evil' is loaded, change to insert state."
(interactive (list (read-string "Note title: ")))
(let* ((deft-use-filename-as-title t)
(zdId (or id
(zetteldeft-generate-id str)))
(zdName (concat zdId zetteldeft-id-filename-separator str)))
(deft-new-file-named zdName)
(when zetteldeft-new-filename-to-kill-ring
(kill-new zdName))
(zetteldeft--insert-title str)
(save-buffer)
(when (featurep 'evil) (evil-insert-state))))
To bypass deft-auto-populate-title-maybe
and prevent having two lines with title matter, we need make sure deft-use-filename-as-title
is set to t.
Note that the file is only actually created when save-buffer
is called.
Similar to the previous function, but also insert a link to the newly created note.
The generated ID is passed to zetteldeft-new-file
.
;;;###autoload
(defun zetteldeft-new-file-and-link (str)
"Create a new note and insert a link to it.
Similar to `zetteldeft-new-file', but insert a link to the new file."
(interactive (list (read-string "Note title: ")))
(let ((zdId (zetteldeft-generate-id str)))
(zetteldeft--insert-link zdId str)
(zetteldeft-new-file str zdId)))
A further extension includes both a link and a backlink:
- a link in the original note
- a backlink in the new note
This obviously only works when the new note is branched out from within a Zetteldeft note (which is verified by zetteldeft--check
).
Store a link, generate a new one, and we’re done.
;;;###autoload
(defun zetteldeft-new-file-and-backlink (str)
"Create a new note and insert link and backlink."
(interactive (list (read-string "Note title: ")))
(let ((ogId (zetteldeft--current-id))
(zdId (zetteldeft-generate-id str)))
(zetteldeft--insert-link zdId str)
(zetteldeft-new-file str zdId)
(newline)
(zetteldeft--insert-link ogId (zetteldeft--id-to-title ogId))))
Highlight a region of an existing note and call zetteldeft-extract-region-to-note
to extract the region into a new note.
This function will prompt for a title.
Once entered, a new note is created with that title and the highlighted region as its content.
The original highlighted region will be replaced with a link to this new note.
As with other link insertions, the format of this link can be customized through the zetteldeft-insert-link-function
variable.
(defun zetteldeft-extract-region-to-note (title)
"Extract the marked region to a new note with TITLE."
(interactive (list (if (not (use-region-p))
(user-error "No region active.")
(read-string "Note title: "))))
(let* ((id (zetteldeft-generate-id title))
(text (kill-region (region-beginning) (region-end))))
(save-excursion
(zetteldeft-new-file title id)
(yank)
(save-buffer))
(zetteldeft--insert-link id title)))
This is a wrapper function to follow links to a file.
When point is in a link, open the note it links to (unless zetteldeft-follow-at-point
is nil
).
Otherwise, call avy
to jump to and open a selected link.
;;;###autoload
(defun zetteldeft-follow-link ()
"Use Avy to follow a link.
Follows zetteldeft link to a file if point is on a link.
Uses Avy to prompt for a link to follow with `zetteldeft-avy-file-search'
if it isn't. Variable `zetteldeft-follow-at-point' controls this last
option: when nil, always use Avy."
(interactive)
(if (and zetteldeft-follow-at-point
(thing-at-point-looking-at (zetteldeft--link-regex)))
(zetteldeft--search-filename
(zetteldeft--lift-id (zetteldeft--get-thing-at-point)))
(zetteldeft-avy-file-search)))
A little boolean that allows customization whether zetteldeft-follow-link
should automatically open links at point.
Set this to nil
to always select a link with avy
.
(defcustom zetteldeft-follow-at-point t
"Should `zetteldeft-follow-link' open link at point?
When t, open note at point if point is on a link.
When nil, always use Avy."
:type 'boolean
:group 'zetteldeft)
A function to conveniently browse notes: keep on jumping from note to note.
Meanwhile use keys such as .
, <
and >
to go to the home note, previous buffer or next buffer.
All while you keep browsing and following links.
Since avy-jump
returns t
after each succesful jump (and zetteldeft-avy-file-search
passes this on), we can use it in a loop.
;;;###autoload
(defun zetteldeft-browse ()
"Browse your notes with avy.
Keep calling `zetteldeft-avy-file-search' in a loop."
(interactive)
(let ((avy-single-candidate-jump nil)
(avy-handler-function 'zetteldeft--browse-avy-handler))
(while (zetteldeft-avy-file-search)
(message "Browsing in Zetteldeft! [.] home [<] prev [>] next"))))
Setting avy-single-candidate-jump
to nil prevents unexpected jumps when there’s only one Zetteldeft link candidate on screen.
Now let’s add some custom keys to the default avy
setup.
For reference, here are some notes on how avy
works:
- The variable
avy-dispatch-alist
lists actions of what to do when a key not onavy-keys
is pressed during dispatch. The provided action is what is executed once a candidate is finaly pressed. - The
avy-handler-default
function processes bad read keys. Changed with the variableavy-handler-function
.
So, let’s define a separate handler which is used in the browse function above. Based on the default handler function.
(defun zetteldeft--browse-avy-handler (char)
"The default handler for a bad CHAR."
(let (dispatch)
(cond ((setq dispatch (assoc char avy-dispatch-alist))
(unless (eq avy-style 'words)
(setq avy-action (cdr dispatch)))
(throw 'done 'restart))
((memq char avy-escape-chars)
;; exit silently
(throw 'done 'abort))
;; Go to home note
((eq char ?.)
(zetteldeft-go-home)
(message "Brwosing to home note.")
(zetteldeft-browse)
(throw 'done 'abort))
;; Previous buffer
((eq char ?<)
(previous-buffer)
(message "Browsing to previous buffer.")
(zetteldeft-browse)
(throw 'done 'abort))
((eq char ?>)
(next-buffer)
(message "Browsing to next buffer.")
(zetteldeft-browse)
(throw 'done 'abort))
((eq char ??)
(avy-show-dispatch-help)
(throw 'done 'restart))
((mouse-event-p char)
(signal 'user-error (list "Mouse event not handled" char)))
(t
(message "No such candidate: %s, hit `C-g' to quit."
(if (characterp char) (string char) char))))))
Use avy to jump to a tag and search for it.
The search term should include the #
as tag identifier, so it’s as easy as jumping to the #
and running zetteldeft-search-at-point
.
Avy uses zetteldeft-tag-regex
as a regular expression.
;;;###autoload
(defun zetteldeft-avy-tag-search ()
"Call on avy to jump to a tag.
Tags are filtered with `zetteldeft-tag-regex'."
(interactive)
(save-excursion
(let ((avy-all-windows nil))
(when (consp (avy-jump zetteldeft-tag-regex))
(zetteldeft-search-at-point)))))
Use avy to jump to a link and find the corresponding file. Since the ID should be unique, there should be only one result. That file is then opened (in another window if requested).
Links are found by concatenating zetteldeft-link-indicator
, zetteldeftd-id-regex
and zetteldeft-link-suffix
.
;;;###autoload
(defun zetteldeft-avy-file-search (&optional otherWindow)
"Use `avy' to follow a zetteldeft link.
Links are found via `zetteldeft-link-indicator' and `zetteldeft-id-regex'.
Open that file (in another window if OTHERWINDOW)."
(interactive)
(save-excursion
(when (consp (avy-jump (zetteldeft--link-regex)))
(zetteldeft--search-filename
(zetteldeft--lift-id (zetteldeft--get-thing-at-point)) otherWindow))))
Some notes:
- Function
avy-jump
returns a cons cell if the character is found and otherwise returnst
. That’s why the(when (consp
is needed. - The optional
otherWindow
is passed tozetteldeft--search-filename
, and from there todeft-open-file
.
Let’s also define a function to open a file in another window of choice.
Selection of the window occurs via ace-window
.
(declare-function aw-select "ace-window")
When only one window is open, split it first.
ace-window
will select the other window automatically when only two are available.
;;;###autoload
(defun zetteldeft-avy-file-search-ace-window ()
"Use `avy' to follow a zetteldeft link in another window.
Similar to `zetteldeft-avy-file-search', but with window selection.
When only one window is active, split it first.
When more windows are active, select one via `ace-window'."
(interactive)
(save-excursion
(when (consp (avy-jump (zetteldeft--link-regex)))
(let ((ID (zetteldeft--lift-id (zetteldeft--get-thing-at-point))))
(when (eq 1 (length (window-list))) (split-window))
(select-window (aw-select "Select window..."))
(zetteldeft--search-filename ID)))))
Use avy to jump to a link and search for its ID in deft.
This means that each note containing this ID is found.
If you want to open the note with the ID as its name (i.e., follow a link), use zetteldeft-avy-file-search
.
;;;###autoload
(defun zetteldeft-avy-link-search ()
"Use `avy' to perform a deft search on a zetteldeft link.
Similar to `zetteldeft-avy-file-search' but performs a full
text search for the link ID instead of filenames only.
Opens immediately if there is only one result."
(interactive)
(save-excursion
(when (consp (avy-jump (zetteldeft--link-regex)))
(zetteldeft--search-global
(zetteldeft--lift-id (zetteldeft--get-thing-at-point))))))
Over time, you might delete notes you no longer deem relevant, or that you have merged with other notes. Links to those notes are now dead (or rather: broken). To prune dead links, we need to find them first.
Finding all dead links works as follows:
- Find all links in all Zetteldeft notes.
- Check whether they have a valid destination, i.e. whether they appear in a filename.
- If they don’t, add them to the list of dead links.
(defun zetteldeft--list-dead-links ()
"Return a list with IDs in Zetteldeft notes that have no corresponding note."
(let ((dead-links '())
(deft-filter-only-filenames t))
(dolist (link (zetteldeft--list-all-links))
(deft-filter link t)
(when (eq 0 (length deft-current-files))
(unless (member link dead-links)
(push link dead-links))))
dead-links))
Show a buffer zith all dead Zetteldeft links and where they are found. Easy enough by constructing a couple of loops.
First, the name of the buffer:
(defconst zetteldeft--dead-links-buffer-name "*zetteldeft-dead-links*")
Now for the function itself.
(defun zetteldeft-dead-links-buffer ()
"Show a buffer with all dead links in Zetteldeft."
(interactive)
(switch-to-buffer zetteldeft--dead-links-buffer-name)
(erase-buffer)
(message "Finding all dead Zetteldeft links...")
(let ((dead-links (zetteldeft--list-dead-links)))
(insert (format "# Found %d dead links\n" (length dead-links)))
(dolist (link dead-links)
(insert (format " - %s in: " link))
(deft-filter link t)
(dolist (source (deft-current-files))
(zetteldeft--insert-link (zetteldeft--lift-id source)))
(insert "\n")))
(unless (eq major-mode 'org-mode) (org-mode)))
The following function launches deft, clears the filter and enters evil-insert-state
(when evil is used).
;;;###autoload
(defun zetteldeft-deft-new-search ()
"Launch deft, clear filter and enter insert state."
(interactive)
(deft)
(deft-filter-clear)
(when (featurep 'evil) (evil-insert-state)))
A quick but necessary check to see whether the provided file is part of the deft directory.
To achieve this, first check whether the buffer is visiting a file.
When that is the case, take the path of the file the buffer is currently visiting, and check whether the deft-directory
is part of that.
Signal a user error if it is not.
The file-truename
is there to make sure that deft-directory
is first expanded to an absolute path before comparing it to the file name of the current buffer (which is already an absolute path).
(defun zetteldeft--check ()
"Check if the currently visited file is in `zetteldeft' territory:
whether it has `deft-directory' somewhere in its path."
(unless (buffer-file-name)
(user-error "Buffer not visiting a file"))
(unless (string-match-p
(regexp-quote (file-truename deft-directory))
(file-truename (buffer-file-name)))
(user-error "Not in zetteldeft territory")))
Easy enough.
(defun zetteldeft--current-id ()
"Retrieve ID from current file."
(zetteldeft--check)
(zetteldeft--lift-id
(file-name-base (buffer-file-name))))
Easily insert the title of the current file. Used for generating a new file and renaming a file.
First, a customizable prefix to include before the title.
(defcustom zetteldeft-title-prefix "#+TITLE: "
"Prefix string included when `zetteldeft--insert-title' is called.
Formatted for `org-mode' by default.
Don't forget to include a space."
:type 'string
:group 'zetteldeft)
Second, a custom string inserted after the title.
Below the title, an additional template string is inserted automatically.
This string, variable zetteldeft-title-suffix
, can be customized and is empty by default.
(defcustom zetteldeft-title-suffix ""
"String inserted below title when `zetteldeft--insert-title' is called.
Empty by default.
Don't forget to add `\\n' at the beginning to start a new line."
:type 'string
:group 'zetteldeft)
Now the function itself. It gets the base of the buffer file name, takes from it the file title (i.e. strips the link id at the beginning), and inserts the remaining string.
(defun zetteldeft--insert-title (title)
"Insert TITLE as title in file.
Prepended by `zetteldeft-title-prefix' and appended by `zetteldeft-title-suffix'."
(zetteldeft--check)
(insert
zetteldeft-title-prefix
title
zetteldeft-title-suffix))
Returns the file title from a file.
By default, deft-parse-title
is used to do so, meaning Zetteldeft respects Deft title rules the user has configured.
(Note that deft-use-filename-as-title
will always be nil
, see below.)
For additional flexibility, the variable zetteldeft-title-parsing-function
can be customized, but the function this points to should be compatible with deft-parse-title
, meaning it should take two arguments (full path to the note and contents of the note).
(defcustom zetteldeft-title-parsing-function #'deft-parse-title
"Function used to extract a title from a note; defaults to `deft-parse-title'.
The function you use here must be compatible with `deft-parse-title': the
first argument is the file's fully qualified path; the second is the contents
of said file."
:type 'function
:group 'zetteldeft)
The zetteldeft--lift-file-title
function itself then uses the customized function.
(defun zetteldeft--lift-file-title (zdFile)
"Return the title of a zetteldeft note.
ZDFILE should be a full path to a note."
(let ((deft-use-filename-as-title nil))
(funcall zetteldeft-title-parsing-function
zdFile
(with-temp-buffer
(insert-file-contents zdFile)
(buffer-string)))))
For this to work, we need to make sure deft-use-filename-as-title
is nil, otherwise deft-parse-title
will just return the filename.
Rename the current note. Prompt for a new title, use the new title to also change the filename (relying on deft name rules), and update the title in the note.
When the current file has no Zetteldeft ID, one is generated.
This means that zetteldeft-file-rename
can be used to easily generate IDs for notes that have none.
;;;###autoload
(defun zetteldeft-file-rename ()
"Change current file's title, and use the new title to rename the file.
Use this on files in the `deft-directory'.
When the file has no Zetteldeft ID, one is generated and included in the new name."
(interactive)
(zetteldeft--check)
(let ((old-filename (buffer-file-name)))
(when old-filename
(let* ((old-title (zetteldeft--lift-file-title old-filename))
(prompt-text (concat "Change " old-title " to: "))
(new-title (read-string prompt-text old-title))
(id (or (zetteldeft--lift-id (file-name-base old-filename))
(zetteldeft-generate-id new-title old-filename)))
(new-filename
(deft-absolute-filename
(concat id zetteldeft-id-filename-separator new-title))))
(rename-file old-filename new-filename)
(deft-update-visiting-buffers old-filename new-filename)
(zetteldeft-update-title-in-file new-title)
(deft-refresh)))))
When renaming a note, the title line will be updated, if present. If the note does not currently contain a title line, Zetteldeft will insert one if the following variable is set.
(defcustom zetteldeft-always-insert-title t
"When renaming a note, insert title if not already present."
:type 'boolean
:group 'zetteldeft)
To update the title of the currently visited file, the following function is used.
It simply looks for the zetteldeft-title-prefix
, deletes that line, and replaces it with a new title line.
If no title is present and zetteldeft-always-insert-title
is set, a new title
line is inserted.
A limitation of this workflow is that it will not work when the zetteldeft-title-prefix
has a new line in it.
(defun zetteldeft-update-title-in-file (title)
"Update the title in the current note buffer to TITLE.
This searches the buffer for `zetteldeft-title-prefix' and updates the current
title, if present. If not present and `zetteldeft-always-insert-title' is set,
this inserts a title line at the beginning of the buffer. Otherwise, no change
is made."
(save-excursion
(let ((zetteldeft-title-suffix ""))
(goto-char (point-min))
(if (re-search-forward (regexp-quote zetteldeft-title-prefix) nil t)
(progn (delete-region (line-beginning-position) (line-end-position))
(zetteldeft--insert-title title))
(when zetteldeft-always-insert-title
(zetteldeft--insert-title title)
(newline))))))
To count the total number of words, lets loop over all the files and count words in each. The total is printed in the minibuffer.
;;;###autoload
(defun zetteldeft-count-words ()
"Prints total number of words and notes in the minibuffer."
(interactive)
(let ((numWords 0))
(dolist (deftFile deft-all-files)
(with-temp-buffer
(insert-file-contents deftFile)
(setq numWords (+ numWords (count-words (point-min) (point-max))))))
(message
"Your zettelkasten contains %s notes with %s words in total."
(length deft-all-files) numWords)))
Add the ID from the current file to the kill ring.
Steps:
- Get the filename from the buffer
- Strip the ID from it.
- Wrap the ID with
zetteldeft-link-indicator
andzetteldeft-link-suffix
to create a full link. - Result can be empty string when no id is detected in the filename.
;;;###autoload
(defun zetteldeft-copy-id-current-file ()
"Copy current ID.
Add the id from the filename the buffer is currently visiting to the
kill ring."
(interactive)
(zetteldeft--check)
(let ((ID (concat zetteldeft-link-indicator
(zetteldeft--lift-id (file-name-base (buffer-file-name)))
zetteldeft-link-suffix)))
(kill-new ID)
(message "%s" ID)))
Return a list of links found in a specified file.
A minor bug here: this actually returns IDs rather than real Zetteldeft links.
(defun zetteldeft--extract-links (deftFile)
"Find all links in DEFTFILE and return a list."
(let ((zdLinks (list)))
(with-temp-buffer
(insert-file-contents deftFile)
(while (re-search-forward zetteldeft-id-regex nil t)
(let ((foundTag (replace-regexp-in-string " " "" (match-string 0))))
;; Add found tag to zdLinks if it isn't there already
(unless (member foundTag zdLinks)
(push foundTag zdLinks)))
;; Remove found tag from buffer
(delete-region (point) (re-search-backward zetteldeft-id-regex))))
zdLinks))
A function that returns a list of all links found in Zetteldeft notes.
Relies on zetteldeft--extract-links
.
(defun zetteldeft--list-all-links ()
"Return a list with all IDs that appear in notes."
(let ((all-links '()))
(dolist (file deft-all-files)
(dolist (link (zetteldeft--extract-links file))
(unless (member link all-links)
(push link all-links))))
all-links))
Convert a Zetteldeft ID into its full path.
The ID should lead to only one file, obviously, so an error is thrown when this is not the case.
(defun zetteldeft--id-to-full-path (zdID)
"Return full path from given zetteldeft ID ZDID.
Returns nil when no files are found.
Throws an error when multiple files are found."
(let ((deft-filter-only-filenames t))
(deft-filter zdID t))
(when (> (length deft-current-files) 1)
(user-error "ID Error. Multiple zetteldeft files found with ID %s" zdID))
(car deft-current-files))
Convert a Zetteldeft ID into its full path.
The ID should lead to only one file, obviously, so an error is thrown when this is not the case.
(defun zetteldeft--id-to-title (zdId)
"Turn a Zetteldeft ID into the title."
(zetteldeft--lift-file-title
(zetteldeft--id-to-full-path zdId)))
Tags are a way to quickly retrieve a subsection of notes.
To quickly insert a tag, use zetteldeft-tag-insert
: it will prompt for a tag to be added at the end of the first line that starts with zetteldeft-tag-line-prefix
.
If, however, this variable is set to nil
, tags will be inserted at point.
Want to insert a tag at point even with zetteldeft-tag-line-prefix
configured?
Use zetteldeft-tag-insert-at-point
.
Note: variable zetteldeft-tag-regex
is used to find tags, zetteldeft-tag-prefix
for inserting tags.
To fully customize tag searching and inserting behaviour to your liking, you might have to modify both.
This regular expression indicates what tags can look like so that they can be found.
By default, tags start with a #
or @
and contain least one or more lower case letters.
Dashes are allowed.
(defcustom zetteldeft-tag-regex "[#@][[:alnum:]_-]+"
"Regular expression for finding Zetteldeft tags."
:type 'string
:group 'zetteldeft)
Note that this regular may trip over #
symbols used in URLS.
Newly created Zetteldeft tags start with a #
by default, but this can be customized.
Note that this (currently) only affects how tags are inserted via zetteldeft-tag-insert
– it doesn’t change which tags are found.
(defcustom zetteldeft-tag-prefix "#"
"String prefix used when inserting new Zetteldeft tags."
:type 'string
:group 'zetteldeft)
A string indicating the line where newly added tags should go. Tags are added at the end of the first line that matches this string.
(defcustom zetteldeft-tag-line-prefix "# Tags"
"String used to find the line where tags in Zetteldeft files should go."
:type 'string
:group 'zetteldeft)
Insert a Zetteldeft tag at point. When called interactively, prompt user to select a tag from the list of existing tags (or enter a new tag).
;;;###autoload
(defun zetteldeft-tag-insert-at-point (tag)
"Insert TAG at point. Interactively, select an existing tag or provide new one."
(interactive (list (completing-read
"Tag to insert: "
(zetteldeft--get-all-sorted-tags))))
(unless (string-prefix-p zetteldeft-tag-prefix tag)
(insert zetteldeft-tag-prefix))
(insert tag))
The unless
ensures that a newly entered tag includes the tag prefix.
Append a Zetteldeft tag to the tag line (or, when the tag line isn’t found, insert it at point).
The tag line is the first line that starts with zetteldeft-tag-line-prefix
.
The user is prompted to select a tag from the list of existing tags, or enter a new one.
To always insert tag at point, set zetteldeft-tag-line-prefix
to nil.
;;;###autoload
(defun zetteldeft-tag-insert ()
"Select existing tag or enter new one to insert in current Zetteldeft note.
The tag is appended to the first line starting with `zetteldeft-tag-line-prefix'.
If this variable is nil, or tag line is not found, insert tag at point."
(interactive)
(zetteldeft--check)
(let ((dest (when zetteldeft-tag-line-prefix
(save-excursion
(goto-char (point-min))
(re-search-forward zetteldeft-tag-line-prefix nil t)))))
(if dest
(save-excursion
(goto-char dest)
(end-of-line)
(insert " ")
(call-interactively 'zetteldeft-tag-insert-at-point))
(call-interactively 'zetteldeft-tag-insert-at-point))))
Easily remove a tag from the currently visited note.
Prompt for a tag via completing-read
and remove the first instance of it.
Steps:
- Get tags in current buffer
- Select one of these tags
- Search buffer for tag string
- Delete first instance of found tag (including potential space before)
(defun zetteldeft-tag-remove ()
"Prompt for a tag to remove from the current Zetteldeft note.
Only the first instance of the selected tag is removed."
(interactive)
(zetteldeft--check)
; Extract tags of current file into `zetteldeft--tag-list'
(setq zetteldeft--tag-list (list))
(save-buffer)
(zetteldeft--extract-tags (buffer-file-name))
; Select a tag from that list
(let* ((tag (completing-read
"Tag to remove: "
(seq-filter 'stringp zetteldeft--tag-list))))
; Find and remove first instance of that tag
(save-excursion
(goto-char (point-min))
(re-search-forward tag nil t)
(delete-region (point) (re-search-backward tag nil t))
; remove potential empty space before tag
(backward-char)
(when (looking-at " ") (delete-char 1)))))
Let’s display all tags in Zetteldeft.
With zetteldeft-tag-buffer
they are all gathered in a buffer, including how often they appear.
The name of the buffer we’ll be using:
(defconst zetteldeft--tag-buffer-name "*zetteldeft-tag-buffer*")
And some code to create that buffer.
It works like so:
- Move to the
zetteldeft--tag-buffer-name
and clear it. - Gather all tags in a property list, as strings with an integer counting how often they appear.
This is what
zetteldeft--get-all-tags
achieves. - Loop through that list and print strings and counts.
- Sort results alphabetically.
;;;###autoload
(defun zetteldeft-tag-buffer ()
"Switch to the `zetteldeft-tag-buffer' and list tags."
(interactive)
(switch-to-buffer zetteldeft--tag-buffer-name)
(erase-buffer)
(let ((tagList (zetteldeft--get-all-tags)))
(dolist (zdTag tagList)
(when (stringp zdTag)
(insert (format "%s (%d) \n"
zdTag
(lax-plist-get tagList zdTag)))))
(unless (eq major-mode 'org-mode) (org-mode))
(sort-lines nil (point-min) (point-max))
(goto-char (point-min))))
Note: the zetteldeft--get-all-tags
returns a property list, with alternating tags (i.e., a string) and how often they appear (i.e., a number).
To only act on the strings, we use a when (stringp zdTag)
.
First, we need a variable to store the tags in.
(defvar zetteldeft--tag-list nil
"A temporary property list to store all tags.")
Extracting tags with zetteldeft--extract-tags
.
- Loop through all files in Zetteldeft and extract tags from them
- Return this list
(defun zetteldeft--get-all-tags ()
"Return a plist of all the tags found in zetteldeft files."
(setq zetteldeft--tag-list (list))
(dolist (deftFile deft-all-files)
(zetteldeft--extract-tags deftFile))
zetteldeft--tag-list)
An expansion of the previous function, which ensures found tags are sorted.
(defun zetteldeft--get-all-sorted-tags ()
"Return a sorted plist of all the tags found in zetteldeft files."
(seq-sort 'string-lessp
(seq-filter 'stringp
(zetteldeft--get-all-tags))))
Some utility functions to achieve all of this.
The regular expression used to filter out tags, zetteldeft-tag-regex
works, but doesn’t filter strictly enough.
Hashtags used in URLs are also found, for example.
That’s why we can make the existing regex more precise by stating that tags should be positioned either be at the beginning of a new line, or preceded by a space.
(defun zetteldeft--tag-format ()
"Adjust `zetteldeft-tag-regex' for more accurate results."
(concat "\\(^\\|\s\\)" zetteldeft-tag-regex))
This means that search results now (could) include a space in front of the tag.
These spaces are removed in the zetteldeft--extract-tags
function, when adding the found tags to the tag buffer.
To extract tags from a single file:
- Open a given file in a temporary buffer.
- Loop a search for the tag regexp.
- When a tag is found, remove any white space from it and add it to the list and increase its count with
zetteldeft--tag-count
. - Delete the found tag and search again.
(defun zetteldeft--extract-tags (deftFile)
"Find all tags in DEFTFILE and add them to `zetteldeft--tag-list'.
Increase counters as we go."
(with-temp-buffer
(insert-file-contents deftFile)
(while (re-search-forward (zetteldeft--tag-format) nil t)
(let ((foundTag (replace-regexp-in-string " " "" (match-string 0))))
;; Add found tag to zetteldeft--tag-list if it isn't there already
(zetteldeft--tag-count foundTag))
;; Remove found tag from buffer
(delete-region (point) (re-search-backward (zetteldeft--tag-format))))))
Check if a provided tag is already in the tag property list. If it isn’t add it and set counter to 1. If it is, increase its counter.
(defun zetteldeft--tag-count (zdTag)
(let ((tagCount (lax-plist-get zetteldeft--tag-list zdTag)))
(if tagCount
; if the tag was there already, inc by 1
(setq zetteldeft--tag-list
(lax-plist-put zetteldeft--tag-list zdTag (1+ tagCount)))
; if tag was not there yet, add & set to 1
(setq zetteldeft--tag-list
(lax-plist-put zetteldeft--tag-list zdTag 1)))))
In this section:
Sometimes you want to easily gather all notes with a certain tag or search term. This is an important step in creating structure in your notes. Zetteldeft can generate a such “list of links”.
There are two types of list of links:
- Plain list of notes with a specific search term, listing both ID and note title for each one.
Such a list can be generated with
zetteldeft-insert-list-links
. - Same as the above, but only listing IDs missing from the current note.
In other words, the list won’t include link to notes that are already linked to from the current buffer.
Generated via
zetteldeft-insert-list-links-missing
.
To make their use more convenient, these functions can be called from an Org-mode source block. This is the real front-end use of this feature.
To easily insert such a block, use zetteldeft-insert-list-links-block
.
The resulting code looks something like this:
(zetteldeft-list-links "#zd-tutorial") - §2021-04-03-2209 Turn org subheading into a new note - §2021-03-29-2226 Tags in Zetteldeft - §2020-11-13-2140 Creating useful links - §2020-10-24-2304 The Zetteldeft Philosphy - §2020-10-06-1014 Exporting with org-publish - §2020-09-22-0855 Backlinks - §2020-09-20-2106 The home note
Hitting C-c C-c
in the source block updates the list below the block.
Additional arguments to the zetteldeft-list-links
function limit the missing links and sorting of the list.
Creates and inserts a list with links to all files with selected search term.
The code gets a list of files that contain the search string, runs through said list and inserts a link for each entry.
When called from a note within zetteldeft, exclude the note itself from the generated list.
This is necessary so that when called from an org
code block within a note, the note itself is not included (since it will be found by deft
, as the search string will be part of that note).
To achieve this, get the full file name of the current buffer, and remove it from the search results if its found there.
The when
part is there so that this deletion is not attempted if the current buffer is not visiting a file.
;;;###autoload
(defun zetteldeft-insert-list-links (string)
"Search for SEARCH-STRING and insert list of links to results."
(interactive (list (read-string "search string: ")))
(let ((result-files (zetteldeft--get-file-list string))
(this-file (buffer-file-name)))
(when this-file
(setq result-files (delete this-file result-files)))
(dolist (file result-files)
(zetteldeft--list-entry-file-link file))))
Does the same as the above function, but only inserts IDs that aren’t already present in the current file.
In contrast with zetteldeft-insert-list-links
, this function can only be used from within a zetteldeft note.
When no missing links are found, i.e. all the notes with the provided strings are already linked to in the current note, a message is printed instead.
To be able to customize this message, include a defcustom
.
(defcustom zetteldeft-list-links-missing-message
" No missing links with search term =%s= found\n"
"Message to insert when no missing links are found.
This is used by `zetteldeft-insert-list-links-missing'.
%s will be replaced by the search term provided to
this function."
:type 'string
:group 'zetteldeft)
Including missing links is especially handy when you want to check whether all notes with a certain tag are mentioned in a note, or simply to list notes with a specific tag that are not linked to yet (in the current note). Similar to the function above, filter out ID of the current note. In contrast to the function above, this one works with IDs rather than full paths.
A fundamental shortcoming of this piece of code, is that after it is executed, the note now includes the previously missing ID links, which in turn means that on the next run no links will be included…
The most immediate solution is for the user to be wary of this, remove any previously inserted links, save the buffer and refresh the deft cache with deft-refresh
before calling this function.
;;;###autoload
(defun zetteldeft-insert-list-links-missing (string)
"Insert a list of links to all deft files with a search string ZDSRCH.
In contrast to `zetteldeft-insert-list-links' only include links not
yet present in the current file. Can only be called from a file in the
zetteldeft directory."
(interactive (list (read-string "Search string: ")))
(zetteldeft--check)
(let (this-id ; ID of current file
current-ids ; IDs present in current buffer
found-ids ; IDs of notes with search result
final-ids) ; Final list of IDs for the list
;; Gather list of IDs in current buffer
(setq current-ids (zetteldeft--extract-links (buffer-file-name)))
;; Execute search and push found IDs to temporary list
(dolist (file (zetteldeft--get-file-list string))
(push (zetteldeft--lift-id file) found-ids))
;; Keep only unique IDs on the final list
(dolist (id found-ids)
(unless (member id current-ids)
(push id final-ids)))
;; Remove the ID of the current buffer from the final list
(setq this-id (zetteldeft--lift-id (file-name-base (buffer-file-name))))
(setq final-ids (delete this-id final-ids))
;; Finally insert ID and title for each element on the list
(if final-ids
(dolist (id final-ids)
(zetteldeft--list-entry-file-link (zetteldeft--id-to-full-path id)))
;; Unless the list is empty, then insert a message
(insert (format zetteldeft-list-links-missing-message string)))))
The final dolist
contains some ugliness to make things work with zetteldeft-link-suffix
, in order to get the note’s title without the link in it.
Might need to be fixed sometime.
To define what a list entry looks like, let’s create a customizeable string. It determines the list markup.
(defcustom zetteldeft-list-prefix " - "
"Prefix for lists created with `zetteldeft-insert-list-links'
and `zetteldeft-insert-list-links-missing'."
:type 'string
:group 'zetteldeft)
Inserts for given file a link id and title as a list entry.
Only attempt to insert the ID if one is found. When there isn’t (for example when the filename somehow doesn’t contain an ID), don’t try to insert it or we’ll get an error.
(defun zetteldeft--list-entry-file-link (file)
"Insert ZDFILE as list entry."
(let ((id (zetteldeft--lift-id (file-name-base file))))
(insert zetteldeft-list-prefix)
(when id
(insert zetteldeft-link-indicator
id
zetteldeft-link-suffix
" "))
(insert (zetteldeft--lift-file-title file)
"\n")))
The following function is meant to be called from an Org source block. It updates the list below the source block with a new list of links.
The two optional arguments allow to only include missing links, and to sort the resulting list via org-sort-list
.
(defun zetteldeft-list-links (str &optional missing sort)
"Crude attempt to automate `zetteldeft-insert-list-links-missing'.
Meant to be called from an Org source block.
Replaces the list below the source block.
When MISSING is t, use `zetteldeft-insert-list-links-missing'.
When SORT is t, sort the list with most recent at top."
(save-excursion
(org-forward-element)
; Delete any existing list
(when (org-in-item-p)
(delete-region (point) (org-end-of-item-list)))
; New line & insert links
(if missing
(progn (save-buffer)
(deft-refresh)
(zetteldeft-insert-list-links-missing str))
(zetteldeft-insert-list-links str))
(when sort
(org-backward-element)
(org-sort-list t ?A))))
Let’s also automate the insertion of such a code block. This function inserts both the block and the list of links. When called with a prefix, use the “missing”-verion of the list of links.
(defun zetteldeft-insert-list-links-block (str)
"Prompt for a STR to search, and insert an org-mode
source block that calls `zetteldeft-list-links'.
Also include the list of links below the block.
When called with a prefix, make use of the missing links
functions."
(interactive
(list (read-string "Search: ")))
(newline)
(insert "#+BEGIN_SRC emacs-lisp :results silent\n"
"(zetteldeft-list-links \"" str"\"")
(when current-prefix-arg (insert " t"))
(insert ")\n"
"#+END_SRC\n\n")
(if current-prefix-arg
(progn (save-buffer)
(deft-refresh)
(zetteldeft-insert-list-links-missing str))
(zetteldeft-insert-list-links str)))
Org-mode provides Dynamic Blocks, which can programmatically update their contents.
Let’s create a Dynamic Block called zetteldeft-links
.
It will look like this:
#+BEGIN: zetteldeft-links :search "#zd-tutorial" #+END:
When activated (via C-c C-c
), the contents between #+BEGIN:
and #+END:
will be replaced by the search results provided with :search "<string>"
.
Additional options are
:sort t
to sort the results (with most recent note at the top):missing-only t
to only include missing links
The function below inserts such a Dynamic Block.
(defun zetteldeft-org-dblock-insert-links (string)
"Insert an Org Dynamic Block of the `zetteldeft-links' type.
The Dynamic Block takes the following parameters:
- :search 'string', the search string
- :sort t, to sort the results
- :missing-only t, to only include missing links.
"
(interactive
(list (read-string "Search: ")))
(org-create-dblock (list :name "zetteldeft-links" :search string))
(org-update-dblock))
(with-eval-after-load 'org-mode
(org-dynamic-block-define "zetteldeft-links"
'zetteldeft-org-dblock-insert-links))
By including a org-dynamic-block-define
, this dynamic block can now also be inserted via org-dynamic-block-insert-dblock
.
Next, we need a function that fills the dynamic block with search results.
By using the org-dblock-write:
format, Org knows how to compute the dynamic block (see the Org manual https://orgmode.org/manual/Dynamic-Blocks.html).
Through the plist
provided to the Org Dynamic Block we can include functionality to sort the links (with :sort t
), and only include missing links (with :missing-only t
).
(defun org-dblock-write:zetteldeft-links (params)
"Fill the zetteldeft search Org Dynamic Block with contents."
(let ((string (plist-get params :search))
(missing-only (plist-get params :missing-only))
(sort (plist-get params :sort)))
(if missing-only
(zetteldeft-insert-list-links-missing string)
(zetteldeft-insert-list-links string))
(when sort
(org-backward-element)
(org-sort-list t ?A))))
WIP. Relies on Org features, but might work with Markdown.
Exporting notes proceeds in the following steps:
- Setup Org publish project with a
:prepare-function
. See #export-setup for details. - The prepare function takes care of the following:
- Create a temporary directory
zetteldeft--export-tmp-dir
- Convert Zetteldeft links to file links for each file in Zetteldeft:
- Load contents in temporary buffer
- Replace Zetteldeft links with org-mode file links
- Save temporary buffer to file
- Create a temporary directory
There are some drawbacks to this method, notably:
- Only works with Org-mode notes
- During step 2, an error will occur when an ID is not found. This means that a single broken link will prevent exporting.
In the publishing directory, any directory structure is flattened (as the above process loops over all files in Zetteldeft, including subdirectories). This shouldn’t be a problem, though, as all files have unique names.
Exporting revolves around using Org-mode publishing features to export a project.
The codeblock below is only an example, and should be set up by the user.
It works as follows:
- The
:preparation-function zetteldeft--export-prepare-tmp-notes
prepares your notes in a temporary directory - From this temporary directory,
zetteldeft-export-tmp-dir
, export notes to HTML.
Make sure to configure the :publishing-directory
according to your preferences.
(add-to-list 'org-publish-project-alist
`("zetteldeft-notes"
:preparation-function zetteldeft--export-prepare-tmp-notes
:base-directory ,zetteldeft-export-tmp-dir
:publishing-directory "~/notes/Zetteldeft/"
; publishing configuration
:base-extension "org"
:publishing-function org-html-publish-to-html
; org config
:headline-levels 4
:with-tags nil
:with-toc nil
:with-date nil
:with-author nil
; html config
:html-head-extra "<link rel='stylesheet' href='style.css' type='text/css'/>\n"
:html-postamble nil))
We need a temporary directory for the notes with converted links.
By default, use a subfolder in .emacs.d/
(or rather, the user-emacs-directory
).
(defcustom zetteldeft-export-tmp-dir
(expand-file-name "zetteldeft/tmp/" user-emacs-directory)
"Temporary directory for Zetteldeft export")
TO FIX: Issue when dir doesn’t exist
Each file in Deft is copied to the temporary directory and Zetteldeft links are replaced with file links.
First, prepare the temporary directory by deleting and recreating it. Also make the Deft cache is up to date. Then loop over each file, replace links, and copy it to the temporary directory.
(defun zetteldeft--export-prepare-tmp-notes (&optional ignored)
"Copy Zetteldeft files and prepare for export."
(delete-directory zetteldeft-export-tmp-dir t t)
(make-directory zetteldeft-export-tmp-dir t)
(deft-refresh)
(message
"Zetteldeft preparing notes for export at %s"
zetteldeft-export-tmp-dir)
(dolist (file (deft-find-all-files))
(zetteldeft--export-prepare-file file))
(message "Zetteldeft notes copy finished."))
Replacing links is fairly straightforward:
- Find a Zetteldeft link
- Delete the existing link
- Retreive the filename to which it points
- Insert an Org-mode link to the filename
(defun zetteldeft--export-prepare-file (zdFile)
"Prepare ZDFILE for export.
Copy its contents to `zetteldeftd-export-tmp-dir' and replace links with Org
file links. ZDFILE should be the path to the file."
(with-temp-file (expand-file-name
(file-name-nondirectory zdFile)
zetteldeft-export-tmp-dir)
(insert-file-contents zdFile)
(while (re-search-forward (zetteldeft--link-regex) nil t)
(let ((zdLink (match-string 0)))
(delete-region (point)
(re-search-backward (zetteldeft--link-regex)))
(let ((filePath (or (zetteldeft--id-to-full-path
(zetteldeft--lift-id zdLink))
; When ID doesn't return a file (a dead link)
; use empty string
"")))
(insert
(org-make-link-string
(format "./%s" (file-name-nondirectory filePath))
zdLink)))))))
In this section:
Let’s say you want to generate a single Org document containing all notes with a specific tag (or other search term).
There are two functions to achieve this:
- Directly copying the content:
zetteldeft-org-search-insert
inserts the contents of all of these notes below their respective titles. - Using Org-mode
#+INCLUDE
syntax:zetteldeft-org-search-include
generatesorg-mode
syntax to#+INCLUDE
the files.
The following explains what zetteldeft-org-search-include
does, but the concept is more or less the same for zetteldeft-org-search-insert
.
For each of the notes with the provided search term, it inserts a heading, a line with #+INCLUDE
and the full path to the relevant notes.
This results in a single file that can be easily exported.
The only function meant for use on the users end, is zetteldeft-org-search-include
.
For example,
(zetteldeft-org-search-include "#export")
inserts necessary code to include all files containing the tag #export
.
The results would look like the following:
\* First file title
#+INCLUDE: "/path/to/2018-07-13-2210 First file title.org"
\* File two
#+INCLUDE: "/path/to/2018-07-13-2223 File two.org"
All functions are documented below.
You could, for example, add the following code to a document and execute (or evaluate) it from within org-mode
.
Add it under a “comment” type heading to prevent it from being exported itself, like so: * COMMENT Code
.
(let (frst)
(save-excursion
;; Move to next heading
(outline-next-heading)
(setq frst (point))
;; Delete everything after
(delete-region frst (point-max))
;; Include the files
(zetteldeft-org-search-include "#tag")
; Sort these entries alphabetically (set mark to use a region)
; (goto-char frst) (set-mark (point-max))
; (org-sort-entries nil ?a)
))
The code deletes everything after the current header and inserts all notes with #tag
in them.
In order to also sort the entries alphabetically, uncomment the last two lines.
A final caveat: don’t put the file with the above code in you deft
folder, or it will attempt to include itself (since it has #tag
in it).
Before we look at the functions, a note on limitations of the current implementation.
- Over-enthusiastic inclusion Sometimes, a tag appears in a file without the need for it to be included. For example, a file with a list of all tags will also include the tag one wants. In the future, this might be resolved by filtering, for example with http://ergoemacs.org/emacs/elisp_filter_list.html.
- Inclusion from second line onwards
Currently, the
#+INCLUDE
lines only include from the second line onwards. This is a work-around to prevent#+TITLE
lines from being included (and messing up the title onorg-export
. To change this, edit the inserted strings in thezetteldeft--org-include-file
function. - Sorting
The files included are unsorted, or rather: sorted as
deft
provides the results. Attempts at sorting by title are included inzetteldeft--get-file-list
, but not working properly. As a solution, useorg-sort
manually after runningzetteldeft-org-search-include
.
Asks user for a search string and inserts headers and #+INCLUDE
code for all files with said tag.
When used on #tag
, make sure to include the #
manually.
;;;###autoload
(defun zetteldeft-org-search-include (zdSrch)
"Insert `org-mode' syntax to include all files containing ZDSRCH.
Prompt for search string when called interactively."
(interactive (list (read-string "tag (include the #): ")))
(dolist (zdFile (zetteldeft--get-file-list zdSrch))
(zetteldeft--org-include-file zdFile)))
Very similar to the previous function, but rather than writing syntax to include files, insert their contents directly.
;;;###autoload
(defun zetteldeft-org-search-insert (zdSrch)
"Insert the contents of all files containing ZDSRCH.
Files are separated by `org-mode' headers with corresponding titles.
Prompt for search string when called interactively."
(interactive (list (read-string "Search term: ")))
(dolist (zdFile (zetteldeft--get-file-list zdSrch))
(zetteldeft--org-insert-file zdFile)))
Returns the contents of a file.
(defun zetteldeft--file-contents (zdFile &optional removeLines)
"Insert file contents of a zetteldeft note.
ZDFILE should be a full path to a note.
Optional: leave out first REMOVELINES lines."
(with-temp-buffer
(insert-file-contents zdFile)
(when removeLines
(kill-whole-line removeLines))
(buffer-string)))
Inserts the title as a new header, with the #+INCLUDE
line below.
Includes only from the second line onward, so that any #+TITLE
lines are omitted.
(defun zetteldeft--org-include-file (zdFile)
"Insert code to include org file ZDFILE."
(insert
;; Insert org-mode title
"* " (zetteldeft--lift-file-title zdFile) "\n"
;; Insert #+INCLUDE: "file.org" :lines 2-
"#+INCLUDE: \"" zdFile "\" :lines \"2-\"\n\n"))
For a file, insert its title and contents (without first 3 lines).
Even better would be: without any of the lines starting with #
at the beginning of the file.
(defun zetteldeft--org-insert-file (zdFile)
"Insert title and contents of ZDFILE."
(insert
;; Insert org-mode title
"\n* " (zetteldeft--lift-file-title zdFile) "\n\n"
;; Insert file contents (without the first 3 lines)
(zetteldeft--file-contents zdFile 3)))
In this section:
Linking notes together in plain text is fun, but sometimes you want to visualize which notes are connected.
The following functions attempt to provide said functionality, but are in a very early stage of development.
They generate an org source block for graphviz
, which can then be executed to generate a PDF.
A brief introduction:
zetteldeft-org-graph-search
creates a graph with all the notes containing a provided string.zetteldeft-org-graph-note
creates a graph that starts at a note, connects all notes linked to it, and all notes linked to those. In other words, it looks two levels deep.
The resulting graph looks something like this:
It’s worth noting, again, that this is very provisional.
We begin by setting up some customizable parts: syntax that should go at the start and the end of the org-mode
source blocks that will be generated.
Within graphviz, I advise to use fdp
, twopi
(which overlaps more) or circo
as layouts.
(defcustom zetteldeft-graph-syntax-begin
"#+BEGIN_SRC dot :file ./graph.pdf :cmdline -Kfdp -Tpdf
\n graph {\n"
"Syntax to be included at the start of the zetteldeft graph."
:type 'string
:group 'zetteldeft)
This merely closes the source block.
(defcustom zetteldeft-graph-syntax-end
"} \n#+END_SRC\n"
"Syntax to be included at the end of the zetteldeft graph."
:type 'string
:group 'zetteldeft)
An org code block with graphviz
code for a graph.pdf
.
Find all notes with the provided search term. Loop over this list, and insert title and links for each one.
The links are temporarily stored in zetteldeft--graph-links
.
(defvar zetteldeft--graph-links)
Now for the function itself.
;;;###autoload
(defun zetteldeft-org-graph-search (str)
"Insert org source block for graph with zd search results.
STR should be the search the resulting notes of which should be included in the graph."
(interactive (list (read-string "search string: ")))
(setq zetteldeft--graph-links (list))
(let ((zdList (zetteldeft--get-file-list str)))
(insert zetteldeft-graph-syntax-begin)
(insert "\n // links\n")
(dolist (oneFile zdList)
(insert "\n")
(zetteldeft--graph-insert-links oneFile))
(zetteldeft--graph-insert-all-titles))
(insert zetteldeft-graph-syntax-end))
Insert an org source code block for a graphviz presentation of a note and its connections.
When links are added, they are also stored in zetteldeft--graph-links
which is later used to insert titles.
When called interactively, select a file from the completion interface.
;;;###autoload
(defun zetteldeft-org-graph-note (deftFile)
"Create a graph starting from note DEFTFILE."
(interactive (list
(completing-read "Note to start graph from: "
(deft-find-all-files))))
(setq zetteldeft--graph-links (list))
(insert zetteldeft-graph-syntax-begin)
(insert "\n // base note and links \n")
(zetteldeft--graph-insert-links deftFile)
(zetteldeft--graph-insert-additional-links)
(zetteldeft--graph-insert-all-titles)
(insert zetteldeft-graph-syntax-end))
Insert the sanitized ID from the file, followed by an arrow and all of the links.
Store both the deft file provided and any found files in zetteldeft--graph-links
.
(defun zetteldeft--graph-insert-links (deftFile)
"Insert links in DEFTFILE in dot graph syntax on a single line.
Any inserted ID is also stored in `zetteldeft--graph-links'."
(let ((zdId (zetteldeft--lift-id deftFile)))
(when zdId
(insert " \"" zdId "\" -- {")
(dolist (oneLink (zetteldeft--extract-links deftFile))
(zetteldeft--graph-store-link oneLink t)
(insert "\"" oneLink "\" "))
(insert "}\n")
(zetteldeft--graph-store-link deftFile))))
Only do all of these things if a Zetteldeft ID is found in the filename, or else insert
won’t work (as zdID
will be nil).
Titles have to be inserted in the correct graphviz
format, like so:
B [label = "Node B"]
The following function should achieve that.
(defun zetteldeft--graph-insert-title (deftFile)
"Insert the DEFTFILE title definition in a one line dot graph format."
(let ((zdTitle
(replace-regexp-in-string "\"" ""
(zetteldeft--lift-file-title deftFile)))
(zdId (zetteldeft--lift-id deftFile)))
(when zdId
(insert " \"" zdId "\""
" [label = \"" zdTitle " ("
zetteldeft-link-indicator zdId zetteldeft-link-suffix ")\"")
(insert "]" "\n"))
(zetteldeft--graph-store-link deftFile)))
The title is taken from the file string and any additional quotes removed.
For future reference, linked files are stored in zetteldeft--graph-links
.
This function facilitates that process.
Provide a link to a file to store it. Simply providing an ID works too, if you provide the second argument as true.
(defun zetteldeft--graph-store-link (deftFile &optional idToFile)
"Push DEFTFILE to zetteldeft--graph-links unless it's already there.
When IDTOFILE is non-nil, DEFTFILE is considered an id
and the the function first looks for the corresponding file."
(when idToFile
(let ((deft-filter-only-filenames t))
(progn
(deft-filter deftFile t)
(setq deftFile (car deft-current-files)))))
(unless (member deftFile zetteldeft--graph-links)
(push deftFile zetteldeft--graph-links)))
Insert links stored in the zetteldeft--graph-links
list.
Except the first list item, as this is considered the base file already included.
(defun zetteldeft--graph-insert-additional-links ()
"Insert rest of `zetteldeft--graph-links'."
(setq zetteldeft--graph-links (cdr zetteldeft--graph-links))
(dolist (oneFile zetteldeft--graph-links)
(zetteldeft--graph-insert-links oneFile)))
Insert all titles stored in zetteldeft--graph-links
.
(defun zetteldeft--graph-insert-all-titles ()
"Insert graphviz title lines.
Does this for all links stored in `zetteldeft--graph-links'."
(insert "\n // titles \n")
(dolist (oneLink zetteldeft--graph-links)
;; Sometimes, a 'nil' list item is present. Ignore those.
(when oneLink
(zetteldeft--graph-insert-title oneLink))))
Since zetteldeft
doesn’t provide its own minor mode, keybindings should be global.
The function below sets up some defaults. While I’d suggest people integrate these with their personal setup, this function lowers the bar for entry.
Other setups, for evil
or spacemacs
, are suggested in section #suggested-kb.
;;;###autoload
(defun zetteldeft-set-classic-keybindings ()
"Sets global keybindings for `zetteldeft'."
(interactive)
(define-prefix-command 'zetteldeft-prefix)
(global-set-key (kbd "C-c d") 'zetteldeft-prefix)
(global-set-key (kbd "C-c d d") 'deft)
(global-set-key (kbd "C-c d D") 'zetteldeft-deft-new-search)
(global-set-key (kbd "C-c d R") 'deft-refresh)
(global-set-key (kbd "C-c d s") 'zetteldeft-search-at-point)
(global-set-key (kbd "C-c d c") 'zetteldeft-search-current-id)
(global-set-key (kbd "C-c d f") 'zetteldeft-follow-link)
(global-set-key (kbd "C-c d F") 'zetteldeft-avy-file-search-ace-window)
(global-set-key (kbd "C-c d .") 'zetteldeft-browse)
(global-set-key (kbd "C-c d h") 'zetteldeft-go-home)
(global-set-key (kbd "C-c d l") 'zetteldeft-avy-link-search)
(global-set-key (kbd "C-c d t") 'zetteldeft-avy-tag-search)
(global-set-key (kbd "C-c d T") 'zetteldeft-tag-buffer)
(global-set-key (kbd "C-c d /") 'zetteldeft-search-tag)
(global-set-key (kbd "C-c d i") 'zetteldeft-find-file-id-insert)
(global-set-key (kbd "C-c d C-i") 'zetteldeft-full-search-id-insert)
(global-set-key (kbd "C-c d I") 'zetteldeft-find-file-full-title-insert)
(global-set-key (kbd "C-c d C-I") 'zetteldeft-full-search-full-title-insert)
(global-set-key (kbd "C-c d o") 'zetteldeft-find-file)
(global-set-key (kbd "C-c d C-o") 'zetteldeft-full-search-find-file)
(global-set-key (kbd "C-c d n") 'zetteldeft-new-file)
(global-set-key (kbd "C-c d N") 'zetteldeft-new-file-and-link)
(global-set-key (kbd "C-c d B") 'zetteldeft-new-file-and-backlink)
(global-set-key (kbd "C-c d b") 'zetteldeft-backlink-add)
(global-set-key (kbd "C-c d r") 'zetteldeft-file-rename)
(global-set-key (kbd "C-c d x") 'zetteldeft-count-words)
(global-set-key (kbd "C-c d #") 'zetteldeft-tag-insert)
(global-set-key (kbd "C-c d $") 'zetteldeft-tag-remove))
That’s all folks!
(provide 'zetteldeft)
;;; zetteldeft.el ends here
The following assumes deft
is loaded manually in your dotfile, it merely configures the package.
None of these code blocks are tangled into the .el
file, they are here merely as a guide.
Since it doesn’t provide a minor mode, zetteldeft
package doesn’t have it’s own keymap and doesn’t set any keys by default.
It does, however, provide a function to do that for you:
zetteldeft-set-classic-keybindings
.
Calling this will bind some keys behind C-c d
.
To set something else, simply copy this code and do your own thing.
Later paragraphs in this section suggest different setups, namely bindings for evil
and integration with spacemacs
.
Calling zetteldeft-set-classic-keybindings
will set the following (see #kb-defaults):
Key binding | Function |
---|---|
C-c d d | deft |
C-c d D | zetteldeft-deft-new-search |
C-c d R | deft-refresh |
C-c d s | zetteldeft-search-at-point |
C-c d c | zetteldeft-search-current-id |
C-c d f | zetteldeft-follow-link |
C-c d . | zetteldeft-browse |
C-c d F | zetteldeft-avy-file-search-ace-window |
C-c d h | zetteldeft-go-home |
C-c d l | zetteldeft-avy-link-search |
C-c d L | zetteldeft-insert-list-links-block |
C-c d t | zetteldeft-avy-tag-search |
C-c d # | zetteldeft-tag-insert |
C-c d $ | zetteldeft-tag-remove |
C-c d T | zetteldeft-tag-buffer |
C-c d / | zetteldeft-search-tag |
C-c d i | zetteldeft-find-file-id-insert |
C-c d I | zetteldeft-find-file-full-title-insert |
C-c d o | zetteldeft-find-file |
C-c d n | zetteldeft-new-file |
C-c d N | zetteldeft-new-file-and-link |
C-c d B | zetteldeft-new-file-and-backlink |
C-c d b | zetteldeft-backlink-add |
C-c d r | zetteldeft-file-rename |
C-c d x | zetteldeft-count-words |
The setup below is the one I use personally.
It relies on general.el
for modal editing with evil
and configures deft
and zetteldeft
functions behind leader key SPC d
.
(general-define-key
:prefix "SPC"
:non-normal-prefix "C-SPC"
:states '(normal visual motion emacs)
:keymaps 'override
"d" '(nil :wk "deft")
"dd" '(deft :wk "deft")
"dD" '(zetteldeft-deft-new-search :wk "new search")
"dR" '(deft-refresh :wk "refresh")
"ds" '(zetteldeft-search-at-point :wk "search at point")
"dc" '(zetteldeft-search-current-id :wk "search current id")
"df" '(zetteldeft-follow-link :wk "follow link")
"dF" '(zetteldeft-avy-file-search-ace-window :wk "avy file other window")
"d." '(zetteldeft-browse :wk "browse")
"dh" '(zetteldeft-go-home :wk "go home")
"dl" '(zetteldeft-avy-link-search :wk "avy link search")
"dL" '(zetteldeft-insert-list-links-block :wk "insert list of links")
"dt" '(zetteldeft-avy-tag-search :wk "avy tag search")
"dT" '(zetteldeft-tag-buffer :wk "tag list")
"d#" '(zetteldeft-tag-insert :wk "insert tag")
"d$" '(zetteldeft-tag-remove :wk "remove tag")
"d/" '(zetteldeft-search-tag :wk "search tag")
"di" '(zetteldeft-find-file-id-insert :wk "insert id")
"d C-i" '(zetteldeft-full-search-id-insert :wk "insert id full search")
"dI" '(zetteldeft-find-file-full-title-insert :wk "insert full title")
"d C-I" '(zetteldeft-full-search-full-title-insert :wk "insert title full search")
"do" '(zetteldeft-find-file :wk "find file")
"d C-o" '(zetteldeft-full-search-find-file :wk "find full search")
"dn" '(zetteldeft-new-file :wk "new file")
"dN" '(zetteldeft-new-file-and-link :wk "new file & link")
"dB" '(zetteldeft-new-file-and-backlink :wk "new file & backlink")
"db" '(zetteldeft-backlink-add :wk "add backlink")
"dr" '(zetteldeft-file-rename :wk "rename")
"dx" '(zetteldeft-count-words :wk "count words"))
For spacemacs
, keybindings are defined in the deft
layer and activated when deft-zetteldeft
is t
(see instructions above).
These keybindings are reproduced below for reference.
Some keys are available globally and some only when in an org-mode
buffer.
Key binding | Function |
---|---|
SPC a n z n | zetteldeft-new-file |
SPC a n z T | zetteldeft-tag-buffer |
SPC a n z s | zetteldeft-search-at-point |
SPC a n z o | zetteldeft-find-file |
Key binding | Function |
---|---|
SPC m z c | zetteldeft-search-current-id |
SPC m z f | zetteldeft-follow-link |
SPC m z t | zetteldeft-avy-tag-search |
SPC m z N | zetteldeft-new-file-and-link |
SPC m z r | zetteldeft-file-rename |
SPC m z i | zetteldeft-find-file-id-insert |
SPC m z I | zetteldeft-find-file-full-title-insert |
SPC m z s | zetteldeft-search-at-point |
SPC m z l | zetteldeft-avy-link-search |
SPC m z F | zetteldeft-avy-file-search-ace-window |
SPC m z o | zetteldeft-find-file |
If these don’t suit you, or you would like to add different keys, doing so is trivial. For example:
(spacemacs/declare-prefix "d" "deft")
(spacemacs/set-leader-keys "df" 'zetteldeft-follow-link)
Note extensions are md
, txt
and org
.
First of this list is the default for new notes.
(setq deft-extensions '("org" "md" "txt"))
Search the deft directory recursively, to include subdirectories.
(setq deft-directory (concat org-directory "/notes/zetteldeft"))
(setq deft-recursive t)
Some personal additions.
Note that these are functional suggestions, and not included in the zetteldeft
package.
A small function to open a file in the other window and shifting focus to it.
That final part is what the t
argument does.
(defun efls/deft-open-other ()
(interactive)
(deft-open-file-other-window t))
Let’s add another function, to simply preview in the other window, i.e. not switch focus to it.
(defun efls/deft-open-preview ()
(interactive)
(deft-open-file-other-window))
To select results from the item list without leaving the insert
state, I add the following keys.
(with-eval-after-load 'deft
(define-key deft-mode-map
(kbd "<tab>") 'efls/deft-open-preview)
(define-key deft-mode-map
(kbd "<s-return>") 'efls/deft-open-other)
(define-key deft-mode-map
(kbd "s-j") 'evil-next-line)
(define-key deft-mode-map (kbd "s-k") 'evil-previous-line))
I tend to write org-mode
titles with #+title:
(i.e., uncapitalized). Also other org-mode
code at the beginning is written in lower case.
In order to filter these from the deft summary, let’s alter the regular expression:
(setq deft-strip-summary-regexp
(concat "\\("
"[\n\t]" ;; blank
"\\|^#\\+[a-zA-Z_]+:.*$" ;;org-mode metadata
"\\)"))
Its original value was \\([\n ]\\|^#\\+[[:upper:]_]+:.*$\\)
.
An overview of changes, in addition to those mentioned at the top of this document:
- 30 Dec 2020: Renaming notes will now insert a title if there is none, thanks to jeffvalk
- 26 Nov: Add
zetteldeft-search-tag
, thanks to maw. - 07 Oct: Add
zetteldeft-dead-links-buffer
to show dead links. - 06 Oct: Add export functionality.
- 20 Sep: Add
zetteldeft-go-home
. - 19 Sep: Throw error when generated IDs are not unique.
- 21 Aug: Add
zetteldeft-new-file-and-backlink
to create new notes. - 20 Aug: Update
zetteldeft-tag-buffer
to include tag frequency. - 23 July: Add customizable
zetteldeft-custom-id-function
for ID generation. - 08 July: Add
ace-window
as top level requirement. - 06 July: Separate filename from title and respect
deft-file-naming-rules
, thanks to PR by natdash. - 04 June: Change
zetteldeft-new-file-and-link
so that it doesn’t generate an ID. Changezetteldeft-new-file
so that it can take an ID (and drop the EMPTY option). - 13 May: Modified default
zetteldeft-tag-regex
to include[:alnum:]
and improvezetteldeft--tag-format
. - 09 May: Introduced
zetteldeft-link-suffix
, which is""
by default. - 15 Apr: Replace
avy-goto-char
withavy-jump
.avy
functions now work even whenzetteldeft-link-indicator
is""
. - 05 Apr: Add
zetteldeft-set-classic-keybindings
for convenience. - 14 Mar: Simplified
zetteldeft--get-thing-at-point
to better work with customized tags. - 14 Mar: Fixed font-lock when customizing the id indicator (thanks to bymoz089).
- 14 Feb 2020: Documentation updates for usage with
spacemacs
(thanks to brunosmmm). - 30 Dec 2019: Minor fixes for MELPA.
- 23 Nov: Update suggested keybindings.
- 10 Nov: Rename namespace prefix
zd-
tozetteldeft-
for MELPA compliance. This requires users to update their keybinding setups. - 9 Nov: Rename internal functions & prepare for MELPA.
- 30 May: Rename
zetteldeft-string-after-title
tozetteldeft-title-suffix
and add customizablezetteldeft-title-prefix
. - 6 Apr: Add
zetteldeft-follow-link
for convenience. - 28 Feb: Update
zetteldeft-avy-tag-search
to useavy-jump
sinceavy--generic-jump
is obsolete. - 16 Feb: include customizable message
zetteldeft-list-links-missing
for when no missing links are found. - 14 Feb: fix
zetteldeft-insert-list-links
and its brotherzetteldeft-insert-list-links-missing
. - 21 Jan: introduction of customizable
zetteldeft-tag-regex
. - 20 Jan 2019: replaced
§
with customizablezetteldeft-link-indicator
. - 18 Dec: insert not yet included links with
zetteldeft-insert-list-links-missing
. - 15 Nov: open link in other window with
zetteldeft-avy-file-search-ace-window
. - 13 Nov: functions to create
graphviz
added. - 4 Nov: add
zetteldeft-tag-buffer
to find all tags - 21 Oct:
zetteldeft
is now a package. - 10 Oct: Insert contents of files with given search term with
zetteldeft-org-search-insert
. - 24 Sep: Count the total number of words in your zetteldeft with
zetteldeft-count-words
. - 18 July 2018: Include a list of links with
zetteldeft-insert-list-links
, or a list of files withzetteldeft-org-search-include
.