changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > core / emacs/lib/inbox.el

changeset 641: 48bcbca019e6
parent: 6c0e4a44c082
child: f58f3b88c49e
author: Richard Westhaver <ellis@rwest.io>
date: Sun, 08 Sep 2024 20:11:35 -0400
permissions: -rw-r--r--
description: org-project-info dblock update
1 ;;; lib/inbox.el --- Inbox API -*- lexical-binding: t -*-
2 
3 ;; Copyright (C) 2023 Richard Westhaver
4 ;; Version: "0.2.0"
5 ;; Keywords: maint, tools, outlines, extensions
6 
7 ;; This program is free software; you can redistribute it and/or modify
8 ;; it under the terms of the GNU General Public License as published by
9 ;; the Free Software Foundation, either version 3 of the License, or
10 ;; (at your option) any later version.
11 
12 ;; This program is distributed in the hope that it will be useful,
13 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 ;; GNU General Public License for more details.
16 
17 ;; You should have received a copy of the GNU General Public License
18 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
19 
20 ;;; Commentary:
21 
22 ;; This is the elisp interface to the CC Inbox system. The main
23 ;; interface is the inbox.org file which manages personal tasks.
24 
25 ;; Users may use `org-capture' to insert tasks and notes into their
26 ;; own `org-inbox-file' and refactor them to a more sensible
27 ;; destination with `org-refile'.
28 
29 ;;; Code:
30 (require 'org)
31 (require 'org-agenda)
32 (require 'default)
33 ;; (require 'uml-mode)
34 (require 'org-expiry)
35 
36 (defgroup inbox nil
37  "CC Inbox")
38 
39 ;;; Vars
40 (defcustom org-inbox-file
41  (concat (file-name-as-directory org-directory) "inbox.org")
42  "Custom inbox file location."
43  :type 'file
44  :group 'inbox)
45 
46 (defcustom org-inbox-date-start-format "<%Y-%m-%d %a>"
47  "Format of DATE_START property timestamp for week headings. See
48 `org-time-stamp-formats' for accepted values."
49  :type 'string
50  :group 'inbox)
51 
52 (defvar org-inbox-buffer-name "*Inbox*"
53  "The name of the org-inbox buffer.")
54 
55 (defvar org-inbox-config-buffer-name "*Inbox Config*"
56  "Then name of the org-inbox configuration buffer.")
57 
58 (defvar org-inbox-properties
59  '("NEXT" "PREV" "FROM" "TO" "OWNER" "PROJECT" "BLOCKER" "VERSION"))
60 
61 (defvar org-inbox-db-schema
62  '(id file node edge contents properties schedule))
63 
64 ;;; Capture
65 (setq org-id-link-to-org-use-id t
66  org-protocol-default-template-key "L")
67 
68 ;; capture templates
69 (setq org-capture-templates
70  `(("i" "inbox-item" entry (file ,org-inbox-file)
71  "* %?\n%i"
72  :empty-lines 1)
73  ("t" "inbox-task" entry (file ,org-inbox-file) "* TODO %^{item}\n")
74  ("n" "inbox-note" entry (file ,org-inbox-file) "* NOTE %^{item}\n%a")
75  ("l" "inbox-link" entry (file ,org-inbox-file)
76  "* LINK %l")
77  ("L" "inbox-protocol-link" entry (file ,org-inbox-file)
78  "* LINK [[%:link][%:description]]\n%:initial" :empty-lines 1)
79  ("w" "inbox-web-link" entry (file ,org-inbox-file)
80  "* LINK %?"
81  :hook (lambda ()
82  (goto-char (pos-eol))
83  (org-web-tools-insert-link-for-url (org-web-tools--get-first-url))))
84  ("1" "current-task-item" item (clock) "%i%?")
85  ("2" "current-task-checkbox" checkitem (clock) "%i%?")
86  ("3" "current-task-region" plain (clock) "%i" :immediate-finish t :empty-lines 1)
87  ("4" "current-task-kill" plain (clock) "%c" :immediate-finish t :empty-lines 1)
88  ("l" "log" item (file+headline "log.org" "log") "%U %?" :prepend t)
89  ("s" "secret" table-line (file+function "krypt" org-ask-location) "| %^{key} | %^{val} |" :immediate-finish t :kill-buffer t)
90  ("N" "note-item" plain (file+function "notes.org" org-ask-location) "%?")))
91 
92 (add-hook 'org-after-todo-state-change-hook #'org-id-get-create)
93 (add-hook 'org-after-todo-state-change-hook #'org-expiry-insert-created)
94 
95 (setq org-default-notes-file (join-paths org-directory "inbox.org")
96  org-capture-use-agenda-date t
97  org-archive-location "archive.org::")
98 
99 ;;; Utils
100 ;; `org-archive-all-done' doesn't work the way we want. This function
101 ;; will archive all done tasks in the current subtree, or the whole file
102 ;; if prefix arg is given.
103 (defun org-archive-done (&optional scope)
104  "archive all tasks with todo-state of 'DONE' or 'NOPE'."
105  (interactive "P")
106  (org-map-entries
107  (lambda ()
108  (org-archive-subtree)
109  (setq org-map-continue-from (org-element-property :begin (org-element-at-point))))
110  "/+DONE|NOPE" scope))
111 
112 (defun org-children-done ()
113  "Mark all sub-tasks in this heading as 'DONE'."
114  (interactive)
115  (org-map-entries
116  (lambda ()
117  (unless (= (org-current-level) 1)
118  (org-todo "DONE"))
119  nil 'tree)))
120 
121 (defmacro with-inbox-buffer (&rest body)
122  `(save-excursion
123  (with-current-buffer (find-file org-inbox-file)
124  ,@body)))
125 
126 (defun org-sort-todo-priority ()
127  "Sorting function used by `org-sort' to sort by todo order
128  followed by priority. Returns a pair of numbers (TODO . PRIO)."
129  (let* ((elt (cadr (org-element-at-point)))
130  (todo (when-let ((kw (plist-get elt :todo-keyword)))
131  (when (stringp kw)
132  (substring-no-properties kw))))
133  (prio (pcase (plist-get elt :priority)
134  ("A" 1)
135  ("B" 2)
136  ("C" 3)
137  (t 2)))
138  (res))
139  ;; FIXME todo states shouldn't be hardcoded
140  (cond
141  ((null todo) (setq res (cons 3 prio)))
142  ((string= todo "WATCH") (setq res (cons 3 prio)))
143  ((string= todo "WAIT") (setq res (cons 1 prio)))
144  ((string= todo "HOLD") (setq res (cons 1 prio)))
145  ((string= todo "WIP") (setq res (cons 1 prio)))
146  ((string= todo "GOTO") (setq res (cons 2 prio)))
147  ((string= todo "TODO") (setq res (cons 2 prio)))
148  ((string= todo "RESEARCH") (setq res (cons 3 prio)))
149  ((string= todo "DONE") (setq res (cons 4 prio)))
150  ((string= todo "NOPE") (setq res (cons 4 prio))))
151  (unless res (setq res (cons 0 prio)))
152  res))
153 
154 (defun org-sort-compare-todo-priority (a b)
155  "Given two cons consisting of (TODO . PRIO), return t if A
156  should come before B."
157  (message "a: %S b: %S" a b)
158  (cond
159  ((< (car a) (car b)) t)
160  ((> (car a) (car b)) nil)
161  ((= (car a) (car b))
162  (cond
163  ((< (cdr a) (cdr b)) t)
164  ((> (cdr a) (cdr b)) nil)))))
165 
166 
167 (defun org-inbox-sort ()
168  "Sort the current heading by todo order followed by priority."
169  (interactive)
170  (with-inbox-buffer
171  (org-sort-entries nil ?f #'org-sort-todo-priority #'org-sort-compare-todo-priority)))
172 
173 (defun org-inbox-compact ()
174  "Assign missing IDs and creation dates, archive DONE tasks."
175  (interactive)
176  (with-inbox-buffer
177  (org-id-update-id-locations)
178  (org-id-add-to-headlines-in-file)
179  (org-archive-done)
180  (org-map-entries #'org-expiry-insert-created)
181  (org-inbox-sort)))
182 
183 (defun org-inbox-open ()
184  "Open `org-inbox-file' or switch to its buffer if already open."
185  (interactive)
186  (if-let ((inbox (get-buffer org-inbox-buffer-name)))
187  (switch-to-buffer inbox)
188  (find-file org-inbox-file)
189  (rename-buffer org-inbox-buffer-name)))
190 
191 (defun org-inbox-close ()
192  "Close the org-inbox and associated buffers."
193  (interactive)
194  (when-let ((inbox (get-buffer org-inbox-buffer-name)))
195  (kill-buffer inbox)))
196 
197 ;;; dblocks
198 
199 ;; summary
200 (defun org-dblock-write:summary (params)
201  "Generate a file or heading summary section.")
202 
203 (defun org-summary ()
204  "Insert or update a summary section.")
205 
206 ;; project-info
207 (defcustom org-project-info-order '(details status churn log files)
208  "Order in which sections of the 'project-info' dblock will appear."
209  :type 'list
210  :group 'inbox)
211 
212 (defun org-dblock-write:project-info (params)
213  "Generate a project-info section.
214 
215 The following keyword parameters can be passed to the info dynamic block:
216 
217 :location Set or override the project location which is inferred by
218  checking for a LOCATION property in the current tree, followed
219  by the value of the `project-current' function.
220 
221 :branch Set or override the project branch to display info for. Default
222  branch name is 'default'.
223 
224 :files When nil don't include the files table.
225 :churn When nil don't include the vc churn report.
226 :log when nil don't include the vc log.
227 :status when nil don't include vc status.
228 :details When nil don't include the project details section."
229  (let ((location (or (when-let ((param (plist-get params :location)))
230  (cl-coerce param 'string))
231  (org-entry-get (point) "LOCATION")
232  (when-let ((kw (org-collect-keywords '("LOCATION"))))
233  (cadar kw))
234  (project-root (project-current))))
235  (point (point))
236  (files (if-let ((val (plist-member params :files)))
237  (cadr val)
238  t))
239  (churn (if-let ((val (plist-member params :churn)))
240  (cadr val)
241  t))
242  (status (if-let ((val (plist-member params :log)))
243  (cadr val)
244  t))
245  (log (if-let ((val (plist-member params :status)))
246  (cadr val)
247  t))
248  (details (if-let ((val (plist-member params :details)))
249  (cadr val)
250  t)))
251  (message "Generating info for project: %s" location)
252  (let* ((project (project-current nil location))
253  (project-name (project-name project))
254  (project-root (project-root project)))
255  (dolist (i org-project-info-order)
256  (pcase i
257  ('details (when details
258  (message "building project details...")
259  (insert "#+CALL: project-details() :dir " project-root "\n")
260  (org-babel-execute-maybe)
261  (org-table-align)))
262  ('status (when status
263  (message "building project status...")
264  (insert "#+CALL: hg-status() :dir " project-root "\n")))
265  ('churn (when churn
266  (message "building project vc churn...")
267  (insert "#+CALL: hg-churn() :dir " project-root "\n")))
268  ('log (when log
269  (message "building project vc log...")))
270  ('files (when files
271  (message "building project file table...")
272  (insert "#+CALL: project-files() :dir " project-root "\n")))))
273  (org-babel-execute-region point (point)))))
274 
275 (defun org-project-info ()
276  "Insert or update a project-info dblock."
277  (interactive)
278  (if (re-search-forward (rx bol "#+BEGIN:" (+ space) "project-info") nil t)
279  (progn
280  (if (fboundp 'org-fold-show-entry)
281  (org-fold-show-entry)
282  (with-no-warnings (org-show-entry)))
283  (beginning-of-line))
284  (org-create-dblock (list :name "project-info")))
285  (org-update-dblock))
286 
287 (defun org-inbox-configure-dblock ()
288  "Configure the current org-inbox-dblock at point."
289  (interactive)
290  (with-demoted-errors "Error: %S"
291  (let* ((beginning (org-beginning-of-dblock))
292  (parameters (org-prepare-dblock)))
293  (org-inbox-show-config-buffer (current-buffer) beginning parameters))))
294 
295 ;;; ui
296 (defun org-inbox-show-config (&optional buffer position parameters)
297  (interactive)
298  (switch-to-buffer org-inbox-config-buffer-name)
299  (erase-buffer)
300  (remove-overlays)
301  (widget-insert "\n\n")
302  (widget-create 'push-button
303  :notify (lambda(_widget &rest _ignore)
304  (with-current-buffer buffer
305  (goto-char position)
306  )
307  (kill-buffer)
308  (org-ctrl-c-ctrl-c))
309  (propertize "Apply" 'face 'font-lock-comment-face))
310  (widget-insert " ")
311  (widget-create 'push-button
312  :notify (lambda (_widget &rest _ignore)
313  (kill-buffer))
314  (propertize "Cancel" 'face 'font-lock-string-face))
315  (use-local-map widget-keymap)
316  (widget-setup))
317 
318 (provide 'inbox)
319 ;; inbox.el ends here