summaryrefslogtreecommitdiff
path: root/org-margin.el
diff options
context:
space:
mode:
Diffstat (limited to 'org-margin.el')
-rw-r--r--org-margin.el288
1 files changed, 288 insertions, 0 deletions
diff --git a/org-margin.el b/org-margin.el
new file mode 100644
index 0000000..992fa97
--- /dev/null
+++ b/org-margin.el
@@ -0,0 +1,288 @@
+;;; org-margin.el --- Org-mode notes for arbitrary documents -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2025 Simon Parri
+
+;; Author: Simon Parri <simonparri@ganzeria.com>
+;; Keywords: multimedia, hypermedia, outlines, docs
+;; Package-Requires: ((emacs "24.3") (org "9.6") (compat "29.1"))
+;; Version: 0.50
+
+;; 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:
+
+;; This package allows users to associate headings in an Org buffer (known as
+;; the "notes" buffer) with links to an arbitrary document (using Org's
+;; linking facilities). It also provides commands to make linking between the
+;; document and the notes easy (`org-margin-doc-set-link' and
+;; `org-margin-org-set-link'), and to navigate between headings with margin
+;; links (`org-margin-org-next-heading' and `org-margin-org-previous-heading').
+;; See these commands, as well as the Customization group, for more
+;; information on how to use this package.
+
+;; For users familiar with the `org-noter' package, this package essentially
+;; does the same thing, but is more generalized, since it uses arbitrary Org
+;; links instead of expecting the medium to be paged.
+
+;; Some uses for this package could be:
+
+;; + To take notes on code using Org file: links (which remmeber the line as
+;; well as the file)
+
+;; + To take notes on a PDF or EPUB file (using `nov.el' to read EPUBs, and my
+;; package `ol-file-paged' for per-page links to DocView and `pdf-tools'
+;; buffers)
+
+;;; Code:
+
+(require 'org)
+
+;; Custom variables
+
+(defgroup org-margin ()
+ "Org-mode notes for arbitrary documents."
+ :group 'org)
+
+(defcustom org-margin-find-org-buffer-functions
+ '(org-margin-find-org-buffer-fallback)
+ "List of functions to be used to find an appropriate Org notes buffer.
+
+They will be called in order with no arguments and the value returned by
+the first one to return non-nil will be used as the notes buffer."
+ :type 'hook
+ :options '(org-margin-find-org-buffer-fallback
+ org-margin-find-other-buffer))
+
+(defcustom org-margin-doc-modes
+ '(doc-view-mode pdf-view-mode nov-mode)
+ "List of modes to be considered as document modes."
+ :type '(set symbol))
+
+(defcustom org-margin-find-doc-buffer-functions
+ '(org-margin-find-doc-buffer-fallback
+ org-margin-find-other-buffer)
+ "List of functions to be used to find an appropriate document buffer.
+
+They will be called in order with no arguments and the value returned by
+the first one to return non-nil will be used as the document buffer."
+ :type 'hook
+ :options '(org-margin-find-doc-buffer-fallback
+ org-margin-find-other-buffer))
+
+(defcustom org-margin-org-fold-others t
+ "When t, org-margin-org command fold other headings after moving.
+
+When this is nil, `org-margin-org-next-heading' and
+`org-margin-org-previous-heading' will leave the folding as-is after
+moving."
+ :type 'boolean)
+
+;; Buffer-finding code
+
+(defun org-margin--find (list)
+ "Search LIST of functions for the first one that returns non-nil.
+
+Returns the value returned by the function.
+
+Each function is called with no arguments."
+ (cl-loop for f in list
+ do (when-let ((it (funcall f)))
+ (cl-return it))))
+
+(defun org-margin-find-other-buffer ()
+ "Get the buffer from the \"other\" window.
+
+Other window is determined by `other-window-for-scrolling'."
+ (with-selected-window (other-window-for-scrolling)
+ (current-buffer)))
+
+(defun org-margin-find-org-buffer-fallback ()
+ "Try to find an Org-mode buffer that is currently displayed.
+
+First tries the \"other\" window (as determined by
+`other-window-for-scrolling'), then tries any other displayed window.
+
+Returns nil if no appropriate buffer is found."
+ (or
+ (with-selected-window (other-window-for-scrolling)
+ (when (derived-mode-p 'org-mode)
+ (current-buffer)))
+ (when-let ((w (cl-find-if
+ (lambda (x)
+ (with-selected-window x
+ (derived-mode-p 'org-mode)))
+ (window-list))))
+ (with-selected-window w
+ (current-buffer)))))
+
+(defun org-margin-find-org-buffer ()
+ "Find an appropriate Org buffer for the current document buffer.
+
+Uses `org-margin-find-org-buffer-functions', which see for how to
+customize its behavior."
+ (org-margin--find org-margin-find-org-buffer-functions))
+
+(defun org-margin-find-doc-buffer-fallback ()
+ "Find a relevant buffer in one of `org-margin-doc-modes'.
+
+Goes through all visible windows and returns the first displayed buffer
+that is in one of the modes listed in `org-margin-doc-modes'."
+ (when-let ((w (cl-find-if
+ (lambda (x)
+ (with-selected-window x
+ (derived-mode-p org-margin-doc-modes)))
+ (window-list))))
+ (with-selected-window w
+ (current-buffer))))
+
+(defun org-margin-find-doc-buffer ()
+ "Find an appropriate Org buffer for the current document buffer.
+
+Uses `org-margin-find-doc-buffer-functions', which see for how to
+customize its behavior."
+ (org-margin--find org-margin-find-org-buffer-functions))
+
+(cl-defmacro org-margin--with-buffer ((buffer &optional error) &rest body)
+ "Evaluate BODY in BUFFER, raising ERROR if BUFFER is nil.
+
+If ERROR is not given, a generic \"No buffer found\" error is raised
+instead."
+ (declare (indent 1))
+ (let ((b (gensym 'b)))
+ `(if-let ((,b ,buffer))
+ (with-current-buffer ,b
+ ,@body)
+ (user-error ,(or error "No buffer found.")))))
+
+;; Margin-doc mode code
+
+;;;###autoload
+(defun org-margin-doc-set-link ()
+ "Link the current heading in the notes buffer to the current view.
+
+Uses `org-store-link' to get the most appropriate link for the current
+buffer and position, and stores it in the MARGIN_LINK property of the
+current heading in the notes buffer (as found by
+`org-margin-find-org-buffer')."
+ (interactive nil org-margin-doc-mode)
+ (let ((link
+ (let (org-stored-links)
+ (org-store-link nil))))
+ (org-margin--with-buffer
+ ((org-margin-find-org-buffer)
+ "No Org buffer found.")
+ (org-entry-put nil "MARGIN_LINK" link))))
+
+(defvar-keymap org-margin-doc-mode-map
+ "C-M-." 'org-margin-doc-set-link)
+
+;;;###autoload
+(define-minor-mode org-margin-doc-mode
+ "Minor mode for Org-Margin in the document buffer."
+ :lighter " Margin-Doc"
+ :interactive t)
+
+;; Margin-org mode code
+
+;;;###autoload
+(defun org-margin-org-set-link ()
+ "Link the current heading to the current view of the document buffer.
+
+See `org-margin-find-doc-buffer' for how the document buffer is
+determined and `org-margin-doc-set-link' for how the link is determined
+and set."
+ (interactive nil org-mode)
+ (org-margin--with-buffer
+ ((org-margin-find-doc-buffer)
+ "No other window.")
+ (org-margin-doc-set-link)))
+
+(defun org-margin-org-get-link ()
+ "Get the margin link of the current heading."
+ (when (derived-mode-p 'org-mode)
+ (org-entry-get nil "MARGIN_LINK")))
+
+;;;###autoload
+(defun org-margin-org-open-link ()
+ "Open the margin link of the current heading."
+ (interactive nil org-mode)
+ (let ((w (selected-window)))
+ (org-link-open-from-string
+ (or (org-margin-org-get-link)
+ (user-error "No link for this heading.")))
+ (select-window w t)))
+
+(defun org-margin-org--goto-heading-with-property (dir)
+ "Go to the nearest heading with a MARGIN_LINK property in direction DIR.
+
+If DIR is greater than 0, look forwards, otherwise look backwards.
+
+Returns the value of the heading's MARGIN_LINK property."
+ (org-back-to-heading)
+ (if (> dir 0)
+ (outline-next-heading)
+ (outline-previous-heading))
+ (while (not (or (org-entry-get nil "MARGIN_LINK")
+ (if (> dir 0)
+ (eobp)
+ (bobp))))
+ (if (> dir 0)
+ (outline-next-heading)
+ (outline-previous-heading)))
+ (org-entry-get nil "MARGIN_LINK"))
+
+;;;###autoload
+(defun org-margin-org-next-heading (&optional arg)
+ "Move to the next heading with a margin link and follow the link.
+
+With ARG repeats, or moves backward if negative."
+ (interactive "p" org-mode)
+ (unless arg (setq arg 1))
+ (let ((last nil)
+ (point (point)))
+ (when org-margin-org-fold-others
+ (save-excursion
+ (org-back-to-heading-or-point-min)
+ (org-fold-hide-entry)))
+ (dotimes (_ (abs arg))
+ (setq last (org-margin-org--goto-heading-with-property arg)))
+ (org-reveal t)
+ (when org-margin-org-fold-others
+ (org-fold-show-entry t))
+ (if last
+ (org-margin-org-open-link)
+ (goto-char point)
+ (user-error "No other heading with margin link"))))
+
+;;;###autoload
+(defun org-margin-org-previous-heading (&optional arg)
+ "Move to the previous heading with a margin link and follow the link.
+
+With ARG repeats, or moves forward if negative."
+ (interactive "p" org-mode)
+ (org-margin-org-next-heading (- arg)))
+
+(defvar-keymap org-margin-org-mode-map
+ "C-M-." 'org-margin-org-set-link
+ "C-M-," 'org-margin-org-open-link
+ "C-M-n" 'org-margin-org-next-heading
+ "C-M-p" 'org-margin-org-previous-heading)
+
+;;;###autoload
+(define-minor-mode org-margin-org-mode
+ "Minor mode for Org-Margin in the Org mode buffer."
+ :lighter " Margin-Org")
+
+(provide 'org-margin)
+;;; org-margin.el ends here