Skip to content

Instantly share code, notes, and snippets.

@QiangF
Forked from c4droid369/org-zettel.el
Created June 6, 2025 14:19
Show Gist options
  • Select an option

  • Save QiangF/633904a261e763848859ee8377e03d90 to your computer and use it in GitHub Desktop.

Select an option

Save QiangF/633904a261e763848859ee8377e03d90 to your computer and use it in GitHub Desktop.
My homebrew implementation for zettelkasten in org-mode
;;; org-zettel --- Simple zettelkasten implement in Org-mode -*- lexical-binding: t -*-
;; Copyright (C) 2025 c4droid
;; Author: c4droid <[email protected]>
;; URL: https://codeberg.org/c4dr01d/org-zettel
;; Version: 0.1
;; Package-Requires: ((emacs "27.1"))
;; Keywords: org
;;; Commentary:
;; This is my simple note taking workflow base on Org and Zettelkasten.
;;; Code:
(defgroup org-zettel nil
"Simple zettelkasten implement in Org-mode."
:link '(url-link "https://codeberg.org/c4dr01d/org-zettel"))
;;; Customization
(defcustom org-zettel-directory nil
"Zettelkasten note directory."
:type 'string)
(defcustom org-zettel-template-directory nil
"Zettelkasten template directory.")
;;; Keymaps
(defvar org-zettel-map
(let ((map (make-sparse-keymap "org-zettel-map"))
(maps (list "C-c n c" #'org-zettel-create
"C-c n s" #'org-zettel-search
"C-c n l" #'org-zettel-insert-forward-link
"C-c n b" #'org-zettel-insert-backward-link)))
(cl-loop for (key fn) on maps by #'cadr
do (progn
(when (stringp key)
(setq key (kbd key)))
(define-key map key fn)))
map)
"Keymap for org-zettel.")
;;; Commands
;;;###autoload
(defun org-zettel-create ()
"Create a new zettelkasten note with template support."
(interactive)
(let* ((id (org-zettel-id))
(filename (format "%s.org" id))
(title (read-string "Note title: "))
(raw-tags (read-string "Tags (space-separated): "))
(date (format-time-string "%Y-%m-%d"))
(tags (replace-regexp-in-string " +" ":" raw-tags))
(template-file
(when (file-directory-p org-zettel-template-directory)
(let ((templates (directory-files
org-zettel-template-directory t "\\.org\\'")))
(when templates
(completing-read "Choose template: "
(mapcar #'file-name-base templates)
nil t)))))
(template-content
(if template-file
(with-temp-buffer
(insert-file-contents template-file)
(buffer-string))
"#+TITLE: {{TITLE}}\n#+DATE: {{DATE}}\n#+ID: {{ID}}\n#+FILETAGS: :{{TAGS}}:\n\n")))
(find-file (expand-file-name filename org-zettel-directory))
(insert (org-zettel-process-template template-content title date id tags))
(org-mode)
(save-buffer)
(when (y-or-n-p "Add inital links?")
(org-zettel-insert-forward-link)
(when (y-or-n-p "Add backlink to other notes?")
(org-zettel-insert-backward-link)))
(goto-char (point-max))))
;;;###autoload
(defun org-zettel-insert-forward-link (&optional target-id)
"Insert forward link and auto save."
(interactive)
(let* ((current-id (org-id-get nil t))
(target-id (or target-id (org-id-get-with-outline-path-completion)))
(target-file (when target-file
(with-current-buffer (find-file-noselect target-file)
(org-zettel-get-note-title)))))
(if target-id
(progn
(save-excursion
(goto-char (point-min))
(if (re-search-forward "^\\* 相关链接" nil t)
(progn
(end-of-line)
(insert (format "\n- [[id:%s][%s]]" target-id (or target-title "Untitled"))))
(goto-char (point-max))
(insert (format "\n* 相关链接\n- [[id:%s][%s]]\n" target-id (or (target-title "Untitled")))))
(save-buffer))
(message "org-zettel-insert-forward-link: Operation cancelled")))))
;;;###autoload
(defun org-zettel-insert-backward-link ()
"Insert backlink to current note."
(interactive)
(let* ((current-id (org-id-get nil t))
(current-title (org-zettel-get-note-title))
(target-id (org-id-get-with-outline-path-completion))
(target-file (and target-id (org-id-find-id-file target-id))))
(when target-id
(with-current-buffer (find-file-noselect target-file)
(save-excursion
(goto-char (point-min))
(if (re-search-forward "^\\* 反向链接" nil t)
(progn
(end-of-line)
(insert (format "\n- [[id:%s][%s]]" current-id current-title)))
(goto-char (point-max))
(insert (format "\n* 反向链接\n- [[id:%s][%s]]\n" current-id current-title)))
(save-buffer))
(message "Backlink added to %s" (file-name-nondirectory target-file))))))
;;;###autoload
(defun org-zettel-search ()
"Searching zettelkasten library."
(interactive)
(let ((keyword (read-string "Search by keyword: ")))
(grep (format "grep -nH -i -e '%s' %s/*.org" keyword org-zettel-directory))))
;;; Functions
;;; Publics
(defun org-zettel-id ()
"Generate unique ID based on time. Format: YYYYMMDDHHSS"
(format-time-string "%Y%m%d%H%M%S"))
(defun org-zettel-process-template (content title date id tags)
"Process template with placeholders and esnure links section."
(let ((processed-content
(replace-regexp-in-string "{{TAGS}}" (format ":%s:" tags)
(replace-regexp-in-string "{{ID}}" id
(replace-regexp-in-string "{{DATE}}" date
(replace-regexp-in-string "{{TITLE}}" title content))))))
(if (string-match "^* 相关链接" processed-content)
processed-content
(concat processed-content "\n* 相关链接\n"))))
(defun org-zettel-get-note-title ()
"Extract note title from current buffer.
Priority order:
1. #+TITLE property
2. First heading
3. Filename (fallback)"
(save-excursion
(save-excursion
(widen)
(goto-char (point-min))
(let (title)
(when (re-search-forward "^#\\+TITLE: *\$.*\$" nil t)
(setq title (match-string 1)))
(unless title
(when (re-search-forward "^\*+ +" nil t)
(setq title (substring-no-properties (thing-at-point 'line t)))
(when title
(setq title (replace-regexp-in-string "^\*+ +" "" title))
(setq title (replace-regexp-in-string " +:[a-zA-Z@:]+:$" "" title)))))
(unless title
(setq title (file-name-base (buffer-file-name))))
(when title
(replace-regexp-in-string "[\n\r]" "" title))))))
;;; Footer
(provide 'org-zettel)
;;; org-zettel.el ends here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment