123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- ;;; jao-org-notes.el --- A simple system for org note taking -*- lexical-binding: t; -*-
- ;; Copyright (C) 2020, 2021, 2022, 2024, 2025 jao
- ;; Author: jao <mail@jao.io>
- ;; Keywords: tools
- ;; 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:
- ;; An org note per file, with consultable title and tags and a
- ;; backlinks approximation.
- ;;; Code:
- (require 'org)
- (require 'consult)
- (require 'jao-shell)
- (defvar jao-org-notes-dir (expand-file-name "notes" org-directory))
- (defun jao-org-notes-list ()
- (directory-files-recursively jao-org-notes-dir "\\.org$"))
- (defun jao-org-notes--rg-cmd (rgx &rest args)
- `("rg" "--null" "--line-buffered" "--color=never" "--max-columns=250"
- "--type=org" "--line-number" "--no-heading" "--smart-case"
- ,@args ,default-directory "-e" ,rgx))
- (defun jao-org-notes--rg-title-or-tags (str)
- (let* ((m (string-match "^\\([^/]+\\)/\\(.*\\)" str))
- (d (or (and m (match-string 1 str)) ""))
- (str (if m (match-string 2 str) str))
- (default-directory
- (if (file-directory-p d) (expand-file-name d) default-directory))
- (ts (mapconcat #'identity (split-string str "[:,]+" t) ":|"))
- (rgx (format "^#.(title: .*%s|(tags:.*(%s:)))" str ts)))
- (jao-org-notes--rg-cmd rgx "-m" "2")))
- (defun jao-org-notes--clean-match (m)
- (list (format "%s %s"
- (replace-regexp-in-string default-directory "" (car m) nil t)
- (replace-regexp-in-string "[0-9]+:#\\+\\(title\\|tags\\):"
- "" (cadr m)))
- (expand-file-name (car m) default-directory)
- (string-to-number (cadr m))))
- (defun jao-org-notes--matches (lines)
- (mapcar (lambda (l) (jao-org-notes--clean-match (split-string l "\0" t))) lines))
- (defun jao-org-notes--grep-rx (rx &rest rg-args)
- (let ((default-directory jao-org-notes-dir))
- (jao-org-notes--matches
- (apply #'jao-shell-cmd-lines (apply #'jao-org-notes--rg-cmd rx rg-args)))))
- (defvar jao-org-notes--grep-history nil)
- (defun jao-org-notes--consult-group (m transform)
- (or (and transform m)
- (and (string-match-p "^[^:]+ + :" m) "tags")
- "titles"))
- (defun jao-org-notes--consult-rg (prompt &optional cat no-req cmd initial)
- (let ((default-directory (expand-file-name (or cat "") jao-org-notes-dir)))
- (consult--read
- (consult--async-pipeline
- (consult--process-collection #'jao-org-notes--rg-title-or-tags)
- (consult--async-transform #'jao-org-notes--matches))
- :prompt prompt
- :initial (or initial "")
- :add-history (thing-at-point 'symbol)
- :require-match (not no-req)
- :category 'jao-org-notes-lookup
- :group 'jao-org-notes--consult-group
- :lookup (lambda (cand cands &rest _)
- (or (cadr (assoc cand cands)) (substring cand 1)))
- :history '(:input jao-org-notes--grep-history))))
- (defun jao-org-notes-cats ()
- (seq-difference (directory-files jao-org-notes-dir) '("." ".." "attic")))
- (defun jao-org-notes--cat ()
- (let* ((cat (completing-read "Top level category: " (jao-org-notes-cats))))
- (cond ((file-exists-p (expand-file-name cat jao-org-notes-dir)) cat)
- ((yes-or-no-p "New category, create?") cat))))
- (defun jao-org-notes--insert-title (&optional title)
- (let* ((cat (jao-org-notes--cat))
- (note (jao-org-notes--consult-rg "Title: " cat t nil title))
- (title (file-name-base note))
- (title (replace-regexp-in-string "^#" "" title)))
- (when (not (string-empty-p title))
- (let* ((base (replace-regexp-in-string " +" "-" (downcase title)))
- (base (replace-regexp-in-string "[^-[:alnum:][:digit:]]" "" base))
- (fname (expand-file-name (concat cat "/" base ".org")
- jao-org-notes-dir))
- (exists? (file-exists-p fname)))
- (find-file fname)
- (when (not exists?)
- (insert "#+title: " title "\n")
- t)))))
- (defun jao-org-notes--find-tag (tag)
- (jao-org-notes--grep-rx (format "^#.tags:.*:%s:" tag) "-m" "1"))
- (defvar jao-org-notes--tags nil)
- (defvar jao-org-notes--tag-history nil)
- (defun jao-org-notes--read-tags ()
- (let* ((tags (completing-read-multiple "Tags: " jao-org-notes--tags nil nil nil
- 'jao-org-notes--tag-history)))
- (setq jao-org-notes--tags (seq-union jao-org-notes--tags tags #'string=))
- tags))
- (defun jao-org-notes--template (k)
- `(,k "Note" plain (file jao-org-notes-create)
- "%(if %:url \"#+link: %:url\" \"\")\n\n- %a\n %i"))
- (defun jao-org-notes-all-tags ()
- (let ((tags nil))
- (dolist (m (jao-org-notes--find-tag ".*"))
- (setq tags (seq-union tags (cdr (split-string (car m) ":" t)))))
- (sort tags #'string<)))
- (defun jao-org-notes-find-for-pdf (&optional file-name)
- "Given a PDF file name, find its org notes counterpart."
- (let* ((file-name (or file-name buffer-file-name))
- (bn (file-name-base file-name))
- (rx (format "%s\\.org$" (regexp-quote bn)))
- (pred (lambda () (string-prefix-p jao-org-notes-dir buffer-file-name))))
- (save-some-buffers nil pred)
- (or (car (directory-files-recursively jao-org-notes-dir rx))
- (let* ((d (completing-read "Notes subdir: " (jao-org-notes-cats) nil t))
- (d (file-name-as-directory d)))
- (expand-file-name (concat d bn ".org") jao-org-notes-dir)))))
- (defun jao-org-notes-open ()
- "Search for a note file, matching tags and titles with completion."
- (interactive)
- (when-let (f (jao-org-notes--consult-rg "Search notes: "))
- (find-file f)))
- (defun jao-org-notes-consult-tags ()
- "Search for a note file, matching all tags with completion."
- (interactive)
- (let* ((tags (jao-org-notes--read-tags))
- (init (concat "^..tags: " (mapconcat #'identity tags " "))))
- (consult-ripgrep jao-org-notes-dir init)))
- (defun jao-org-notes-consult-ripgrep (&optional initial cat)
- (interactive)
- (consult-ripgrep (expand-file-name (or cat "") jao-org-notes-dir) initial))
- (defun jao-org-notes-create (&optional title)
- "Create a new note file, matching tags and titles with completion."
- (interactive)
- (when (jao-org-notes--insert-title title)
- (org-insert-time-stamp (current-time) t t "#+date: " "\n")
- (insert "#+tags: :"
- (mapconcat #'identity (jao-org-notes--read-tags) ":")
- ":\n"))
- (save-buffer)
- (current-buffer))
- (defun jao-org-notes-backlinks ()
- "Show a list of note files linking to the current one."
- (interactive)
- (if-let* ((res (jao-org-notes--grep-rx
- (concat "\\[file:.*" (regexp-quote (buffer-name)) "\\]\\[")))
- (file (completing-read "File: " res nil t nil))
- (entry (assoc file res)))
- (progn (find-file (cadr entry))
- (when-let (line (caddr entry)) (goto-line line)))
- (message "Nobody links here!")))
- (defun jao-org-notes-insert-tags ()
- "Insert a list of tags at point, with completing read."
- (interactive)
- (insert ":" (mapconcat 'identity (jao-org-notes--read-tags) ":") ":"))
- (defun jao-org-notes-insert-link ()
- "Select a note file (with completion) and insert a link to it."
- (interactive)
- (when-let (f (jao-org-notes--consult-rg "Notes file: "))
- (let ((rel-path (file-relative-name f default-directory))
- (title (with-current-buffer (find-file-noselect f)
- (save-excursion
- (goto-char (point-min))
- (when (re-search-forward "^#\\+title: \\(.+\\)" nil t)
- (match-string 1))))))
- (insert (format "[[file:%s][%s]]" rel-path title)))))
- (defun jao-org-notes-stats ()
- (interactive)
- (message "%d notes, %d tags in %s"
- (length (jao-org-notes-list))
- (length jao-org--notes-tags)
- jao-org-notes-dir))
- ;;;###autoload
- (defun jao-org-notes-setup (mnemonic)
- "Set up the notes system, providing a mnemonic character for its org template."
- (setq org-capture-templates
- (add-to-list 'org-capture-templates (jao-org-notes--template mnemonic))
- jao-org-notes--tags (jao-org-notes-all-tags))
- (when (fboundp 'org-capture-upgrade-templates)
- (org-capture-upgrade-templates org-capture-templates)))
- (provide 'jao-org-notes)
- ;;; jao-org-notes.el ends here
|