From dc99ff49fd6b8f68e64729598414d357c4e62f52 Mon Sep 17 00:00:00 2001 From: Protesilaos Stavrou Date: Sun, 24 Nov 2024 16:35:14 +0200 Subject: [PATCH] emacs: add work-progress-code to select a project via org-capture --- .../prot-emacs-modules/prot-emacs-org.el | 65 ++++++++++ emacs/.emacs.d/prot-emacs.org | 122 +++++++++++++++--- emacs/.emacs.d/prot-lisp/prot-org.el | 81 ++++++++++++ 3 files changed, 247 insertions(+), 21 deletions(-) diff --git a/emacs/.emacs.d/prot-emacs-modules/prot-emacs-org.el b/emacs/.emacs.d/prot-emacs-modules/prot-emacs-org.el index 1d8f545e..908aa024 100644 --- a/emacs/.emacs.d/prot-emacs-modules/prot-emacs-org.el +++ b/emacs/.emacs.d/prot-emacs-modules/prot-emacs-org.el @@ -279,6 +279,71 @@ :immediate-finish t :empty-lines 1))) + + (setq org-capture-templates + `(("u" "Unprocessed" entry + (file+headline "tasks.org" "Unprocessed") + ,(concat "* %^{Title}\n" + ":PROPERTIES:\n" + ":CAPTURED: %U\n" + ":END:\n\n" + "%a\n%i%?") + :empty-lines-after 1) + ("p" "Projects in general" entry + (file+headline "tasks.org" "Projects") + ,(concat "* TODO %^{Title} %^g\n" + "%^{How time sensitive it is|SCHEDULED|SCHEDULED|DEADLINE}: %^t\n" + ":PROPERTIES:\n" + ":CAPTURED: %U\n" + ":END:\n\n" + "%a%?") + :empty-lines-after 1) + + ("s" "Add to an existing project" entry + (function prot-org-capture-select-project) + ,(concat "* TODO %^{Title} %^g\n" + "%^{How time sensitive it is|SCHEDULED|SCHEDULED|DEADLINE}: %^t\n" + ":PROPERTIES:\n" + ":CAPTURED: %U\n" + ":END:\n\n" + "%a%?") + :empty-lines-after 1) + + ("p" "Projects in general" entry + (file+headline "tasks.org" "Projects") + ,(concat "* TODO %^{Title} %^g\n" + "%^{How time sensitive it is|SCHEDULED|SCHEDULED|DEADLINE}: %^t\n" + ":PROPERTIES:\n" + ":CAPTURED: %U\n" + ":END:\n\n" + "%a%?") + :empty-lines-after 1) + ("c" "Clock in and do immediately" entry + (file+headline "tasks.org" "Clocked tasks") + ,(concat "* TODO %^{Title}\n" + ":PROPERTIES:\n" + ":EFFORT: %^{Effort estimate in minutes|5|10|15|30|45|60|90|120}\n" + ":END:\n\n" + "%a\n") + :prepend t + :clock-in t + :clock-keep t + :immediate-finish t + :empty-lines-after 1) + ("p" "Private lesson or service" entry + (file "coach.org") + #'prot-org-capture-coach + :prepend t + :empty-lines 1) + ("P" "Private service clocked" entry + (file+headline "coach.org" "Clocked services") + #'prot-org-capture-coach-clock + :prepend t + :clock-in t + :clock-keep t + :immediate-finish t + :empty-lines 1))) + ;; NOTE 2024-11-10: I realised that I was not using this enough, so ;; I decided to simplify my setup. Keeping it here, in case I need ;; it again. diff --git a/emacs/.emacs.d/prot-emacs.org b/emacs/.emacs.d/prot-emacs.org index dd1a642f..ae0ed113 100644 --- a/emacs/.emacs.d/prot-emacs.org +++ b/emacs/.emacs.d/prot-emacs.org @@ -7710,7 +7710,7 @@ As for my workflow, here is an overview: :bind ("C-c c" . org-capture) :config (require 'prot-org) - + (setq org-capture-templates `(("u" "Unprocessed" entry (file+headline "tasks.org" "Unprocessed") @@ -7720,17 +7720,25 @@ As for my workflow, here is an overview: ":END:\n\n" "%a\n%i%?") :empty-lines-after 1) - ;; ("e" "Email note (unprocessed)" entry ; Also see `org-capture-templates-contexts' - ;; (file+headline "tasks.org" "Unprocessed") - ;; ,(concat "* TODO %:subject :mail:\n" - ;; ":PROPERTIES:\n" - ;; ":CAPTURED: %U\n" - ;; ":END:\n\n" - ;; "%a\n%i%?") - ;; :empty-lines-after 1) - ("w" "Add to the wishlist (may do some day)" entry - (file+headline "tasks.org" "Wishlist") - ,(concat "* %^{Title}\n" + ("p" "Projects in general" entry + (file+headline "tasks.org" "Projects") + ,(concat "* TODO %^{Title} %^g\n" + "%^{How time sensitive it is|SCHEDULED|SCHEDULED|DEADLINE}: %^t\n" + ":PROPERTIES:\n" + ":CAPTURED: %U\n" + ":END:\n\n" + "%a%?") + :empty-lines-after 1) + + ("s" "Add to an existing project" plain + (function prot-org-capture-select-project) + "TODO %?" + :immediate-finish t) ; My custom function does all the work + + ("p" "Projects in general" entry + (file+headline "tasks.org" "Projects") + ,(concat "* TODO %^{Title} %^g\n" + "%^{How time sensitive it is|SCHEDULED|SCHEDULED|DEADLINE}: %^t\n" ":PROPERTIES:\n" ":CAPTURED: %U\n" ":END:\n\n" @@ -7748,15 +7756,6 @@ As for my workflow, here is an overview: :clock-keep t :immediate-finish t :empty-lines-after 1) - ("t" "Time-sensitive task" entry - (file+headline "tasks.org" "Tasks with a date") - ,(concat "* TODO %^{Title} %^g\n" - "%^{How time sensitive it is|SCHEDULED|SCHEDULED|DEADLINE}: %^t\n" - ":PROPERTIES:\n" - ":CAPTURED: %U\n" - ":END:\n\n" - "%a%?") - :empty-lines-after 1) ("p" "Private lesson or service" entry (file "coach.org") #'prot-org-capture-coach @@ -14186,6 +14185,87 @@ DEADLINE: %%^T (advice-add 'org-capture-place-template :around 'prot-org--capture-no-delete-windows) (advice-add 'org-add-log-note :around 'prot-org--capture-no-delete-windows) +;;;;; Custom function to select a project to add to + +(defun prot-org--get-outline (&optional file) + "Return `outline-regexp' headings and line numbers of current file or FILE." + (with-current-buffer (find-file-noselect file) + (let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+"))) + candidates) + (save-excursion + (goto-char (point-min)) + (while (if (bound-and-true-p outline-search-function) + (funcall outline-search-function) + (re-search-forward outline-regexp nil t)) + (push + ;; NOTE 2024-11-24: The -5 (minimum width) is a sufficiently high number to keep the + ;; alignment consistent in most cases. Larger files will simply shift the heading text + ;; in minibuffer, but this is not an issue anymore. + (format "%-5s\t%s" + (line-number-at-pos (point)) + (buffer-substring-no-properties (line-beginning-position) (line-end-position))) + candidates) + (goto-char (1+ (line-end-position))))) + (if candidates + (nreverse candidates) + (user-error "No outline"))))) + +(defvar prot-org-outline-history nil + "Minibuffer history for `prot-org-outline-prompt'.") + +(defun prot-org-outline-prompt (&optional file) + "Prompt for outline among headings retrieved by `prot-org--get-outline'. +With optional FILE use the outline of it, otherwise use that of +the current file." + (let ((current-file (or file buffer-file-name)) + (default (car prot-org-outline-history))) + (completing-read + (format-prompt + (format "Select heading inside `%s': " + (propertize (file-name-nondirectory current-file) 'face 'error)) + default) + (prot-common-completion-table-no-sort 'imenu (prot-org--get-outline current-file)) + nil :require-match nil 'prot-org-outline-history default))) + +(defvar prot-org-file-history nil + "Minibuffer history of `prot-org-file-prompt'.") + +(defun prot-org--not-useful-p (file) + "Return non-nil if FILE is not a useful Org file for `org-capture'." + (or (string-match-p "\\.org_archive\\'" file) + (backup-file-name-p file) + (not (string-match-p "\\.org\\'" file)))) + +(defun prot-org-file-prompt () + "Select a file in the `org-directory'." + (if-let ((dir org-directory) + (files (directory-files-recursively org-directory ".*" nil)) + (files (seq-remove #'prot-org--not-useful-p files))) + (let ((default (car prot-org-file-history))) + (completing-read + (format-prompt "Select file" default) + files nil :require-match nil 'prot-org-file-history default)) + (user-error "There are no files in the `org-directory'"))) + +;;;###autoload +(defun prot-org-select-project (file line-with-heading) + (interactive + (let ((f (prot-org-file-prompt))) + (list f (prot-org-outline-prompt f)))) + (pcase-let* ((`(,line ,text) (split-string line-with-heading "\t")) + (line (string-to-number line))) + (with-current-buffer (find-file file) + (goto-char (point-min)) + (forward-line (1- line)) + (org-insert-subheading '(4))))) + +;;;###autoload +(defun prot-org-capture-select-project () + "Like `prot-org-select-project' but specifically for `org-capture'." + (declare (interactive-only t)) + (interactive) + (call-interactively 'prot-org-select-project)) + ;;;; org-agenda (declare-function calendar-day-name "calendar") diff --git a/emacs/.emacs.d/prot-lisp/prot-org.el b/emacs/.emacs.d/prot-lisp/prot-org.el index a8809533..1885506f 100644 --- a/emacs/.emacs.d/prot-lisp/prot-org.el +++ b/emacs/.emacs.d/prot-lisp/prot-org.el @@ -142,6 +142,87 @@ DEADLINE: %%^T (advice-add 'org-capture-place-template :around 'prot-org--capture-no-delete-windows) (advice-add 'org-add-log-note :around 'prot-org--capture-no-delete-windows) +;;;;; Custom function to select a project to add to + +(defun prot-org--get-outline (&optional file) + "Return `outline-regexp' headings and line numbers of current file or FILE." + (with-current-buffer (find-file-noselect file) + (let ((outline-regexp (format "^\\(?:%s\\)" (or (bound-and-true-p outline-regexp) "[*\^L]+"))) + candidates) + (save-excursion + (goto-char (point-min)) + (while (if (bound-and-true-p outline-search-function) + (funcall outline-search-function) + (re-search-forward outline-regexp nil t)) + (push + ;; NOTE 2024-11-24: The -5 (minimum width) is a sufficiently high number to keep the + ;; alignment consistent in most cases. Larger files will simply shift the heading text + ;; in minibuffer, but this is not an issue anymore. + (format "%-5s\t%s" + (line-number-at-pos (point)) + (buffer-substring-no-properties (line-beginning-position) (line-end-position))) + candidates) + (goto-char (1+ (line-end-position))))) + (if candidates + (nreverse candidates) + (user-error "No outline"))))) + +(defvar prot-org-outline-history nil + "Minibuffer history for `prot-org-outline-prompt'.") + +(defun prot-org-outline-prompt (&optional file) + "Prompt for outline among headings retrieved by `prot-org--get-outline'. +With optional FILE use the outline of it, otherwise use that of +the current file." + (let ((current-file (or file buffer-file-name)) + (default (car prot-org-outline-history))) + (completing-read + (format-prompt + (format "Select heading inside `%s': " + (propertize (file-name-nondirectory current-file) 'face 'error)) + default) + (prot-common-completion-table-no-sort 'imenu (prot-org--get-outline current-file)) + nil :require-match nil 'prot-org-outline-history default))) + +(defvar prot-org-file-history nil + "Minibuffer history of `prot-org-file-prompt'.") + +(defun prot-org--not-useful-p (file) + "Return non-nil if FILE is not a useful Org file for `org-capture'." + (or (string-match-p "\\.org_archive\\'" file) + (backup-file-name-p file) + (not (string-match-p "\\.org\\'" file)))) + +(defun prot-org-file-prompt () + "Select a file in the `org-directory'." + (if-let ((dir org-directory) + (files (directory-files-recursively org-directory ".*" nil)) + (files (seq-remove #'prot-org--not-useful-p files))) + (let ((default (car prot-org-file-history))) + (completing-read + (format-prompt "Select file" default) + files nil :require-match nil 'prot-org-file-history default)) + (user-error "There are no files in the `org-directory'"))) + +;;;###autoload +(defun prot-org-select-project (file line-with-heading) + (interactive + (let ((f (prot-org-file-prompt))) + (list f (prot-org-outline-prompt f)))) + (pcase-let* ((`(,line ,text) (split-string line-with-heading "\t")) + (line (string-to-number line))) + (with-current-buffer (find-file file) + (goto-char (point-min)) + (forward-line (1- line)) + (org-insert-todo-subheading '(4))))) + +;;;###autoload +(defun prot-org-capture-select-project () + "Like `prot-org-select-project' but specifically for `org-capture'." + (declare (interactive-only t)) + (interactive) + (call-interactively 'prot-org-select-project)) + ;;;; org-agenda (declare-function calendar-day-name "calendar")