[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] master 18814ca 001/135: Initial Commit.
From: |
Ian Dunn |
Subject: |
[elpa] master 18814ca 001/135: Initial Commit. |
Date: |
Mon, 17 Feb 2020 10:52:42 -0500 (EST) |
branch: master
commit 18814ca5565f9e67a213e37bb167c758989a87ee
Author: Ian D <address@hidden>
Commit: Ian D <address@hidden>
Initial Commit.
---
org-bat.el | 433 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 433 insertions(+)
diff --git a/org-bat.el b/org-bat.el
new file mode 100644
index 0000000..ac2f8b0
--- /dev/null
+++ b/org-bat.el
@@ -0,0 +1,433 @@
+;;; org-bat.el --- Extendable Blockers and Triggers -*- lexical-binding: t; -*-
+
+;; Author: Ian Dunn
+
+;;; Commentary:
+
+;; Finders:
+
+;; - self Self
+;; - siblings Find all siblings of the current entry
+;; - next-sibling Find the next sibling on the same level as the entry
+;; - previous-sibling Find the previous sibling on the same level
+;; - first-child Find the first child of the current entry
+;; - children Find all children of the current entry
+;; - parent Find direct parent of current entry
+;; - descendants Find all descendants of the current entry
+;; - ancestors Find all ancestors of the current entry
+
+;;; Code:
+
+(require 'org)
+(require 'subr-x)
+
+(defun org-bat-parse-form (form)
+ "Form should be KEY(ARGS)."
+ (when (string-match "^\\([!]\\)?\\([a-zA-Z-]+\\)\\(?:(\\([^)]+\\))\\)?" form)
+ (list (intern (match-string 2 form))
+ (when-let (args (match-string 3 form))
+ (save-match-data (split-string args ",")))
+ (match-string 1 form)
+ (match-end 0))))
+
+(defconst org-bat--types
+ '(finder action condition)
+ "Types recognized by org-bat.")
+
+(defun org-bat--function-for-key (key)
+ (cond
+ ((eq key 'consideration)
+ ;; Function is ignored here
+ (cons 'consideration 'identity))
+ (key
+ (when-let ((func-format (format "org-bat-%%s/%s" key))
+ (new-sym
+ ;; Find the first bound function
+ (seq-find
+ (lambda (sym) (fboundp (intern (format func-format sym))))
+ org-bat--types)))
+ (cons new-sym (intern (format func-format new-sym)))))))
+
+(defun org-bat-process-form (form action-or-condition)
+ (let ((targets)
+ (blocking-entry)
+ (form-string form)
+ (consideration 'all)
+ (state nil) ;; Type of operation
+ ;; Keep track of the current headline
+ (last-entry (point-marker)))
+ (while (not (string-empty-p form-string))
+ (pcase-let* ((`(,key ,args ,mod ,new-pos) (org-bat-parse-form
form-string))
+ (`(,type . ,func) (org-bat--function-for-key key)))
+ (unless (and key type func)
+ (user-error "Unrecognized form '%s'" form-string))
+ (setq form-string (string-trim-left (substring form-string new-pos)))
+ (pcase type
+ ('finder
+ (unless (eq state 'finder)
+ ;; We just executed some actions, so reset the entries.
+ (setq targets nil))
+ (setq state 'finder)
+ (let ((markers (apply func args)))
+ ;; Ensure targets is a list so append continues to work
+ (setq targets (append targets (if (consp markers) markers (list
markers))))))
+ ('action
+ (unless (eq action-or-condition 'action)
+ (user-error "Actions aren't allowed in this context."))
+ (unless targets
+ (user-error "Action specified without targets"))
+ (setq state 'action)
+ (dolist (target targets)
+ (org-with-point-at target
+ (apply func last-entry args))))
+ ('condition
+ (unless (eq action-or-condition 'condition)
+ (user-error "Conditions aren't allowed in this context"))
+ (unless targets
+ (user-error "Condition specified without targets"))
+ (setq state 'condition)
+ (unless blocking-entry ;; We're already blocking
+ ;; Check the condition at each target
+ (when-let ((blocks
+ (mapcar
+ (lambda (entry-marker)
+ (org-with-point-at entry-marker
+ (apply func mod args)))
+ targets)))
+ ;; Apply consideration
+ (setq blocking-entry
+ (org-bat-handle-consideration consideration blocks)))))
+ ('consideration
+ ;; Consideration must be at the start of the targets, so clear out
+ ;; any old targets.
+ (setq targets nil)
+ ;; The actual consideration will be the only argument
+ (setq consideration (org-bat-transform-consideration (nth 0
args)))))))
+ ;; We exhausted the input string, but didn't find a condition when we were
+ ;; expecting one.
+ (when (and (eq action-or-condition 'condition) ;; Looking for conditions
+ (eq state 'finder) ;; but haven't found any
+ (not blocking-entry)) ;; ever
+ (when-let ((blocks
+ (mapcar
+ (lambda (entry-marker)
+ (org-with-point-at entry-marker
+ (org-bat-condition/done t)))
+ targets)))
+ ;; Apply consideration
+ (setq blocking-entry
+ (org-bat-handle-consideration consideration blocks))))
+ ;; Only blockers care about the return value, and this will be non-nil if
+ ;; the entry should be blocked.
+ (setq org-block-entry-blocking blocking-entry)
+ (not blocking-entry)))
+
+
+
+(defmacro org-bat-run (change-plist &rest body)
+ (declare (indent 1))
+ `(let* ((pos (plist-get ,change-plist :position))
+ (type (plist-get ,change-plist :type))
+ (from (plist-get ,change-plist :from))
+ (to (plist-get ,change-plist :to)))
+ (if (and
+ ;; We are only handling todo-state-change
+ (eq type 'todo-state-change)
+ ;; And only from a TODO state to a DONE state
+ (member from (cons 'todo org-not-done-keywords))
+ (member to (cons 'done org-done-keywords)))
+ ,@body
+ ;; Return t for the blocker to let the calling function know that there
+ ;; is no block here.
+ t)))
+
+(defun org-bat-trigger-function (change-plist)
+ (org-bat-run change-plist
+ (when-let ((form (org-entry-get pos "TRIGGER")))
+ (org-bat-process-form form 'action))))
+
+(defun org-bat-blocker-function (change-plist)
+ (org-bat-run change-plist
+ (if-let ((form (org-entry-get pos "BLOCKER")))
+ (org-bat-process-form form 'condition)
+ t)))
+
+;;;###autoload
+(defun org-bat-load ()
+ (interactive)
+ (add-hook 'org-trigger-hook 'org-bat-trigger-function)
+ (add-hook 'org-blocker-hook 'org-bat-blocker-function))
+
+;;;###autoload
+(defun org-bat-unload ()
+ (interactive)
+ (remove-hook 'org-trigger-hook 'org-bat-trigger-function)
+ (remove-hook 'org-blocker-hook 'org-bat-blocker-function))
+
+
+
+;; Tag Finder
+(defun org-bat-finder/match (match-spec &optional scope skip)
+ "Find entries with match-spec MATCH-SPEC.
+
+MATCH-SPEC may be any valid match string; it is passed straight
+into `org-map-entries', with 'agenda as the scope.
+
+SCOPE and SKIP are their counterparts in `org-map-entries', but
+as strings.
+
+SCOPE defaults to \"agenda\", and SKIP defaults to nil."
+ (setq scope (if scope (intern scope) 'agenda))
+ (when skip (setq skip (intern skip)))
+ (org-map-entries
+ ;; Find all entries in the agenda files that match the given tag.
+ (lambda nil (point-marker))
+ match-spec scope skip))
+
+;; ID finder
+(defun org-bat-finder/ids (&rest ids)
+ "Find entries with IDs in IDS.
+
+IDS are all UUIDs as understood by `org-id-find'."
+ (mapcar (lambda (id) (org-id-find id 'marker)) ids))
+
+(defun org-bat-finder/self ()
+ (point-marker))
+
+(defun org-bat-finder/siblings ()
+ (org-with-wide-buffer
+ (let ((self (and (ignore-errors (org-back-to-heading t)) (point)))
+ (markers))
+ (org-up-heading-safe)
+ (org-goto-first-child)
+ (push (point-marker) markers)
+ (while (org-get-next-sibling)
+ (unless (equal (point) self)
+ (push (point-marker) markers)))
+ (nreverse markers))))
+
+(defun org-bat-finder/next-sibling ()
+ (org-with-wide-buffer
+ (and (org-get-next-sibling)
+ (point-marker))))
+
+(defun org-bat-finder/previous-sibling ()
+ (org-with-wide-buffer
+ (and (org-get-last-sibling)
+ (point-marker))))
+
+(defun org-bat-finder/first-child ()
+ (org-with-wide-buffer
+ (and (org-goto-first-child)
+ (point-marker))))
+
+(defun org-bat-finder/children ()
+ (org-with-wide-buffer
+ (let ((markers))
+ (org-goto-first-child)
+ (push (point-marker) markers)
+ (while (org-get-next-sibling)
+ (push (point-marker) markers))
+ (nreverse markers))))
+
+(defun org-bat-finder/parent ()
+ (org-with-wide-buffer
+ (and (org-up-heading-safe)
+ (point-marker))))
+
+(defun org-bat-finder/descendants ()
+ (org-with-wide-buffer
+ (org-map-entries
+ (lambda nil (point-marker))
+ nil 'tree)))
+
+(defun org-bat-finder/ancestors ()
+ (org-with-wide-buffer
+ (let ((markers))
+ (while (org-up-heading-safe)
+ (push (point-marker) markers))
+ (nreverse markers))))
+
+(defun org-bat-finder/olp (file path)
+ (org-find-olp (cons file (split-string-and-unquote path "/"))))
+
+(defun org-bat-finder/file (file)
+ ;; If there isn't a buffer visiting file, then there's no point in having a
+ ;; marker to the start of the file.
+ (with-current-buffer (find-file-noselect file)
+ (point-min-marker)))
+
+(defun org-bat-finder/org-file (file)
+ "Finds FILE in `org-directory'."
+ (with-current-buffer (find-file-noselect (expand-file-name file
org-directory))
+ (point-min-marker)))
+
+
+
+;; Set TODO state
+(defun org-bat-action/todo (last-entry new-state)
+ (ignore last-entry)
+ (org-todo new-state))
+
+;; Set planning info
+
+(defun org-bat--mod-timestamp (time-stamp n what)
+ (with-temp-buffer
+ (insert time-stamp)
+ (goto-char (point-min))
+ (org-timestamp-change n what)
+ (buffer-string)))
+
+(defun org-bat--get-planning-info (what)
+ (org-entry-get nil (if (eq what 'scheduled) "SCHEDULED" "DEADLINE")))
+
+(defun org-bat--handle-planning (type last-entry args)
+ (let* ((case-fold-search t)
+ (arg (nth 0 args))
+ (last-ts (org-with-point-at last-entry (org-bat--get-planning-info
type)))
+ (this-ts (org-bat--get-planning-info type))
+ (this-time (and this-ts (org-parse-time-string this-ts)))
+ (current (org-current-time))
+ (current-ts (format-time-string (org-time-stamp-format t) current))
+ (type-map '(("y" . year)
+ ("m" . month)
+ ("d" . day)
+ ("h" . hour)
+ ("M" . minute))))
+ (cond
+ ((member arg '("rm" "remove"))
+ (org-add-planning-info nil nil type))
+ ((member arg '("cp" "copy"))
+ ;; Copy old time verbatim
+ (org-add-planning-info type last-ts))
+ ((string-match-p "\\`[+-]" arg)
+ ;; We support hours and minutes, so this must be supported separately,
+ ;; since org-read-date-analyze doesn't
+ ;; Starts with a + or -, so assume we're incrementing a timestamp
+ (pcase-let* ((`(,n ,what-string ,def) (org-read-date-get-relative arg
this-time current))
+ (ts (if def current-ts this-ts))
+ (what (cdr (assoc-string what-string type-map))))
+ (org--deadline-or-schedule nil type (org-bat--mod-timestamp ts n
what))))
+ (t
+ ;; For everything else, assume `org-read-date-analyze' can handle it
+ (let* ((parsed-time (org-read-date-analyze arg this-time (decode-time
this-time)))
+ (final-time (apply 'encode-time parsed-time))
+ (new-ts (format-time-string "%F %R" final-time)))
+ (org--deadline-or-schedule nil type new-ts))))))
+
+(defun org-bat-action/scheduled (last-entry &rest args)
+ (org-bat--handle-planning 'scheduled last-entry args))
+
+(defun org-bat-action/deadline (last-entry &rest args)
+ (org-bat--handle-planning 'deadline last-entry args))
+
+(defun org-bat-action/tag (last-entry tags)
+ (ignore last-entry)
+ (org-set-tags-to tags))
+
+(defun org-bat-action/set-property (last-entry property value)
+ (ignore last-entry)
+ (org-entry-put nil property value))
+
+(defun org-bat-action/clock-in (last-entry)
+ (ignore last-entry)
+ (org-clock-in))
+
+(defun org-bat-action/clock-out (last-entry)
+ (ignore last-entry)
+ (org-clock-out))
+
+(defun org-bat-action/set-priority (last-entry priority-action)
+ "PRIORITY-ACTION is passed straight to `org-priority'."
+ (ignore last-entry)
+ (org-priority (if (stringp priority-action)
+ (string-to-char priority-action)
+ priority-action)))
+
+;; TODO I will likely want to check the arguments
+(defun org-bat-action/set-effort (last-entry value increment)
+ (ignore last-entry)
+ (org-set-effort value increment))
+
+(defun org-bat-action/archive (last-entry)
+ (ignore last-entry)
+ (org-archive-subtree-default-with-confirmation))
+
+
+
+;; For most conditions, we return true if condition is true and neg is false,
or
+;; if condition is false and neg is true:
+
+;; | cond | neg | res |
+;; |------+-----+-----|
+;; | t | t | f |
+;; | t | f | t |
+;; | f | t | t |
+;; | f | f | f |
+
+;; This means that we want to take the exclusive-or of condition and neg.
+
+(defsubst org-bat--xor (lhs rhs)
+ (or (and lhs (not rhs))
+ (and (not lhs) rhs)))
+
+(defun org-bat-condition/done (neg)
+ (when-let ((condition
+ (if neg
+ (member (org-entry-get nil "TODO") org-not-done-keywords)
+ (member (org-entry-get nil "TODO") org-done-keywords))))
+ (org-get-heading)))
+
+(defun org-bat-condition/todo-state (neg state)
+ (let ((condition (string-equal (org-entry-get nil "TODO") state)))
+ (when (org-bat--xor condition neg)
+ (org-get-heading))))
+
+;; Block if there are headings
+(defun org-bat-condition/headings (neg)
+ (let ((condition (seq-empty-p (org-map-entries (lambda nil t)))))
+ (when (org-bat--xor condition neg)
+ (buffer-name))))
+
+(defun org-bat-condition/variable-set (neg var val)
+ (let ((condition (string-equal (symbol-value (intern var)) (read val))))
+ (when (org-bat--xor condition neg)
+ (format "%s %s= %s" var (or neg "=") val))))
+
+
+
+(defun org-bat-transform-consideration (consideration)
+ (pcase consideration
+ ;; Leave symbols alone
+ ('"all" (intern consideration))
+ ;; Change strings into numbers
+ ((pred stringp)
+ (string-to-number consideration))
+ (_
+ (user-error "Unrecognized consideration '%s'" consideration))))
+
+(defun org-bat-handle-consideration (consideration blocks)
+ (let ((first-block (seq-find #'identity blocks))
+ (total-blocks (seq-length blocks)))
+ (pcase consideration
+ ('all
+ ;; All of them must be fulfilled, so find the first one that isn't.
+ first-block)
+ ((pred integerp)
+ ;; A fixed number of them must be fulfilled, so check how many aren't.
+ (let* ((unfulfilled (seq-count #'identity blocks))
+ (fulfilled (- total-blocks unfulfilled)))
+ (if (> fulfilled consideration)
+ nil
+ first-block)))
+ ((pred floatp)
+ ;; A certain percentage of them must be fulfilled
+ (let* ((unfulfilled (seq-count #'identity blocks))
+ (fulfilled (- total-blocks unfulfilled)))
+ (if (> (/ fulfilled total-blocks) consideration)
+ nil
+ first-block))))))
+
+(provide 'org-bat)
+
+;;; org-bat.el ends here
- [elpa] master updated (487ad1b -> bf8ecda), Ian Dunn, 2020/02/17
- [elpa] master 567e982 007/135: Added package requirements, Ian Dunn, 2020/02/17
- [elpa] master a943aa6 008/135: Added more custom IDs to documentation, Ian Dunn, 2020/02/17
- [elpa] master 21d2019 006/135: Added has-property condition, Ian Dunn, 2020/02/17
- [elpa] master 0e5fd41 004/135: Added .bzrignore., Ian Dunn, 2020/02/17
- [elpa] master 3a4cca9 003/135: Added Makefile., Ian Dunn, 2020/02/17
- [elpa] master 01c50c0 002/135: Make all finders return lists of markers, Ian Dunn, 2020/02/17
- [elpa] master 5010097 005/135: Added initial documentation, Ian Dunn, 2020/02/17
- [elpa] master 18814ca 001/135: Initial Commit.,
Ian Dunn <=
- [elpa] master 1fb6dcf 013/135: Added chain action, Ian Dunn, 2020/02/17
- [elpa] master ad75461 017/135: Use existing org-xor instead of new function, Ian Dunn, 2020/02/17
- [elpa] master adbff7e 019/135: Remove duplicate targets, Ian Dunn, 2020/02/17
- [elpa] master 6886b8a 026/135: Fixed chain-find, Ian Dunn, 2020/02/17
- [elpa] master 55283ff 015/135: Renamed to Org-Edna., Ian Dunn, 2020/02/17
- [elpa] master d348102 025/135: Added file for tests, Ian Dunn, 2020/02/17
- [elpa] master 71cdc0a 027/135: Fixed parsing bug, Ian Dunn, 2020/02/17
- [elpa] master 6b9a676 031/135: Added documentation for each condition, Ian Dunn, 2020/02/17
- [elpa] master 71e00e4 036/135: Fixed consideration handling, Ian Dunn, 2020/02/17
- [elpa] master 9f27779 014/135: Clarified use of case-fold-search in `org-bat--handle-planning', Ian Dunn, 2020/02/17