;;; jao-org-focus.el --- focusing on org subtrees -*- lexical-binding: t; -*- ;; Copyright (C) 2025, 2026 Jose Antonio Ortega Ruiz ;; Author: Jose Antonio Ortega Ruiz ;; Keywords: docs ;; 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 . (require 'org) (require 'cl-lib) (defvar-local jao-org-focus--parent nil) (defvar-local jao-org-focus--section nil) (defvar jao-org-focus-siblings '("escaleta.org" . "novela.org")) (defun jao-org-focus--header-title () (when-let* ((elem (org-element-at-point)) (header (if (eq 'headline (org-element-type elem)) elem (org-previous-visible-heading 1) (org-element-at-point)))) (org-element-property :title header))) (defun jao-org-focus--is-outline () (string= (car jao-org-focus-siblings) (file-name-nondirectory (or (buffer-file-name) "")))) (defun jao-org-focus--jump-to-title (file title) (let* ((file (if (bufferp file) (buffer-file-name file) file)) (link (format "[[%s::*%s]]" file title))) (org-link-open-from-string link))) (defun jao-org-focus-jump-to-sibling () (interactive) (when-let* ((title (save-excursion (jao-org-focus--header-title))) (file (if (jao-org-focus--is-outline) (cdr jao-org-focus-siblings) (car jao-org-focus-siblings))) (link (format "[[./%s::*%s]]" file title))) (jao-org-focus--jump-to-title (format "./%s" file) title))) ;;; focus on subtree (defun jao-org-focus () "Pop creatingly to an indirect buffer focused on the encloing subtree. When invoked on an indirect buffer, pops back to its base." (interactive) (when (jao-org-focus--is-outline) (jao-org-focus-jump-to-sibling)) (if-let* ((b (get-buffer (or jao-org-focus--parent "")))) (let ((title (save-excursion (goto-char (point-min)) (jao-org-focus--header-title)))) (jao-org-focus--jump-to-title b title)) (when-let* ((title (jao-org-focus--header-title)) (parent (buffer-name)) (bname (format "%s [%s]" title parent))) (if-let* ((b (get-buffer bname))) (pop-to-buffer b) (clone-indirect-buffer bname t) (org-focus-mode -1) (org-focus-child-mode) (setq jao-org-focus--parent parent jao-org-focus--section title) (org-narrow-to-subtree) (show-subtree) (count-words (point-min) (point-max)))))) (defun jao-org-focus-redisplay () "Redisplay a focused buffer. Useful when its parent has been reorganised and the narrowing is out of sync." (interactive) (when-let* ((title jao-org-focus--section)) (widen) (goto-char (point-min)) (when (re-search-forward (format "\\*+ %s" title) nil t) (org-narrow-to-subtree) (goto-char (point-min))))) (defun jao-org-focus-redisplay-children () "Find focused children and redisplay them." (interactive) (dolist (b (jao-org-focus-list)) (with-current-buffer b (save-excursion (jao-org-focus-redisplay))))) (defun jao-org-focus-list (&optional any-parent exclude) "List of buffers that are focusing on a subtree of this one or its parent." (let ((n (or jao-org-focus--parent (buffer-name)))) (seq-filter (lambda (b) (let ((p (buffer-local-value 'jao-org-focus--parent b))) (and p (or any-parent (string= n p)) (or (not exclude) (not (string= (buffer-name b) exclude)))))) (buffer-list)))) (defvar jao-org-focus--focused-history nil) (defun jao-org-focus-switch (arg) "Read with completion a focused child and pop to it. With arg, offer to switch to all children, regardless of their parent." (interactive "P") (let ((fl (mapcar 'buffer-name (jao-org-focus-list arg (buffer-name))))) (unless fl (error "No focused children")) (pop-to-buffer (completing-read "Focused child: " fl nil t nil 'jao-org-focus--focused-history)))) (define-minor-mode org-focus-mode "A mode where keeping track of focused children is on by default." :lighter " ◎" :keymap '(("\C-ce" . jao-org-focus-jump-to-sibling) ("\C-cl" . jao-org-focus-switch) ("\C-cR" . jao-org-focus-redisplay) ("\C-co" . jao-org-focus) ("\C-cw" . count-words)) (if org-focus-mode (add-hook 'after-save-hook #'jao-org-focus-redisplay-children nil t) (remove-hook 'after-save-hook #'jao-org-focus-redisplay-children t))) (define-minor-mode org-focus-child-mode "A mode for the children of a focused org buffer." :lighter " ◉" :keymap org-focus-mode-map (when (and org-focus-child-mode (require 'symbol-overlay nil t)) (face-remap-reset-base 'symbol-overlay-default-face) (face-remap-add-relative 'symbol-overlay-default-face 'warning 'underline) (symbol-overlay-mode 1))) (with-eval-after-load 'symbol-overlay (keymap-set org-focus-mode-map "M-n" 'symbol-overlay-jump-next) (keymap-set org-focus-mode-map "M-p" 'symbol-overlay-jump-prev)) (provide 'jao-org-focus) ;;; jao-org-focus.el ends here