;;; jao-org-notes.el --- A simple system for org note taking -*- lexical-binding: t; -*- ;; Copyright (C) 2020, 2021 jao ;; Author: jao ;; 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 . ;;; Commentary: ;; An org note per file, with consultable title and tags and a ;; backlinks approximation. ;;; Code: (require 'org) (require 'consult) (defvar jao-org-notes-dir (expand-file-name "notes" org-directory)) (defvar jao-org-notes--rg (concat "rg --null --line-buffered --color=ansi --max-columns=250" " --no-heading --line-number --smart-case" " . -e \"^(#.(title|filetags): .*)ARG\" OPTS")) (defvar jao-org-notes--trx ":[0-9]+:#\\+\\(file\\)?\\(title\\|tags\\):") (defun jao-org-notes--clean-match (m) (cons (replace-regexp-in-string jao-org-notes--trx " (\\2)" (car m)) (cdr m))) (defun jao-org-notes--matches (lines) (let ((ms (consult--grep-matches lines))) (mapcar #'jao-org-notes--clean-match ms))) (defun jao-org-notes--lookup (_ cands cand) (or (cadr (assoc cand cands)) cand)) (defvar jao-org-notes--grep-history nil) (defun jao-org--grep (prompt &optional cat no-req) (let ((default-directory (expand-file-name (or cat "") jao-org-notes-dir))) (consult--read (consult--async-command jao-org-notes--rg (consult--async-transform jao-org-notes--matches)) :prompt prompt :lookup #'jao-org-notes--lookup :initial consult-async-default-split :add-history (concat consult-async-default-split (thing-at-point 'symbol)) :require-match (not no-req) :category 'jao-org-notes-lookup :history '(:input jao-org-notes--grep-history) :sort nil))) (defun jao-org-notes--cat () (let* ((cats (seq-difference (directory-files jao-org-notes-dir) '("." ".." "attic"))) (cat (completing-read "Top level category: " cats))) (cond ((file-exists-p (expand-file-name cat jao-org-notes-dir)) cat) ((yes-or-no-p "New category, create?") cat) (t (jao-roam--cat))))) (defun jao-org-notes--insert-title () (let* ((cat (jao-org-notes--cat)) (title (file-name-base (jao-org--grep "Title: " cat t))) (title (replace-regexp-in-string "^#" "" title))) (when (not (string-empty-p title)) (let* ((base (replace-regexp-in-string " +" "-" (downcase title))) (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))))) (defvar jao-org-notes--tags nil) (defun jao-org-notes--update-tags () (setq jao-org-notes--tags (org-global-tags-completion-table (directory-files-recursively jao-org-notes-dir ".*\\.org")))) (defun jao-org-notes--read-tags () (unless jao-org-notes--tags (jao-org-notes--update-tags)) (completing-read-multiple "Tags: " jao-org-notes--tags)) (defun jao-org-notes--insert-tags () (insert "#+filetags: " (mapconcat #'identity (jao-org-notes--read-tags) " ") "\n")) (defun jao-org-notes--insert-date () (insert "#+date: ") (org-insert-time-stamp (current-time)) (insert "\n")) (defun jao-org-notes--template (k) `(,k "Note" plain (file jao-org-notes-open-or-create) "\n- %a " :immediate-finish t :jump-to-captured t)) ;;;###autoload (defun jao-org-notes-open () "Search for a note file, matching tags and titles with completion." (interactive) (when-let (f (jao-org--grep "Search notes: ")) (find-file f))) ;;;###autoload (defun jao-org-notes-open-or-create () "Open or create a new note file, matching tags and titles with completion." (interactive) (when (jao-org-notes--insert-title) (jao-org-notes--insert-date) (jao-org-notes--insert-tags) (jao-org-notes--update-tags) (insert "#+link: ")) (save-buffer) (buffer-file-name)) ;;;###autoload (defun jao-org-notes-backlinks () "Show a list of note files linking to the current one." (interactive) (consult-ripgrep jao-org-notes-dir (concat "\\[\\[file:(.*/)?" (regexp-quote (buffer-name))))) ;;;###autoload (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) " "))) ;;;###autoload (defun jao-org-notes-insert-link () "Select a note file (with completion) and insert a link to it." (interactive) (when-let (f (jao-org--grep "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))))) ;;;###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))) (add-to-list 'org-agenda-files jao-org-notes-dir) (when (fboundp 'org-capture-upgrade-templates) (org-capture-upgrade-templates org-capture-templates))) (provide 'jao-org-notes) ;;; jao-org-notes.el ends here