-
-
Save QiangF/633904a261e763848859ee8377e03d90 to your computer and use it in GitHub Desktop.
My homebrew implementation for zettelkasten in org-mode
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;;; 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