diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e697ac..c846a8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: - 28.2 steps: - name: Set up Emacs - uses: purcell/setup-emacs@v3.0 + uses: purcell/setup-emacs@master with: version: ${{matrix.emacs_version}} diff --git a/Eldev b/Eldev index 27eb716..1d09737 100644 --- a/Eldev +++ b/Eldev @@ -1,9 +1,10 @@ -; -*- mode: emacs-lisp; lexical-binding: t; no-byte-compile: t -*- +; -*- mode: emacs-lisp; lexical-binding: t; -*- ;; Autodetermined by `eldev init'. -(eldev-use-package-archive 'gnu) +(eldev-use-package-archive 'gnu-elpa) (eldev-use-package-archive 'melpa) (setq eldev-test-framework 'buttercup) (eldev-add-extra-dependencies 'test 'with-simulated-input) +(eldev-add-extra-dependencies 'test 'dash) diff --git a/README.org b/README.org index e11a1bb..5c07342 100644 --- a/README.org +++ b/README.org @@ -1,9 +1,12 @@ * NOTICE -This is org-gtd 2.0.0. +This is org-gtd 3.0.0. -Check documentation in [[doc/]] if you're upgrading. Please report all defects as Github issues. +Check documentation in [[doc/]] (and the info manual within emacs itself) if you're upgrading. Please report all defects as Github issues. -Use existing tags if you'd rather stick to a pre-2.0 version. +Use existing tags if you'd rather stick to a pre-3.0 version. + +* Sponsorship +I've put many hours of reading, research, and coding, to put this together. If it delivers value to you, helps you manage your life, please consider sponsoring ([[https://github.com/sponsors/Trevoke/][Github sponsors]] or [[https://www.patreon.com/LokiConsulting][Patreon]]) and allowing me to continue putting work into this package and other projects. * Org GTD This package tries to replicate as closely as possible the GTD workflow. @@ -14,6 +17,7 @@ This package provides a system that allows you to capture incoming things into a For a comprehensive instruction manual, see the documentation. Either the info file or in the [[doc/]] directory of the repository. Upgrade information is also available therein. + * Directory tree - =dev= :: used as a jail environment. Spin up with ~$ HOME="dev/" emacs~. - =doc= :: where the documentation lives @@ -21,7 +25,7 @@ Upgrade information is also available therein. * Community If you want help, you can open an issue right on Github. -You're also welcome to join my [[https://discord.gg/9UAXpCaVJb][discord server]] for all conversations related to org-gtd in particular and GTD in general. Many of the ideas for org-gtd came out of my reading the GTD book, and then reading some sections multiple times, but I am in no way an expert. Defining the GTD domain, which is to say, getting to clear nomenclature with clear actions, is still a work in progress. +You're also welcome to join my [[https://discord.gg/2kAK6TfqJq][discord server]] for all conversations related to org-gtd in particular and GTD in general. Many of the ideas for org-gtd came out of my reading the GTD book, and then reading some sections multiple times, but I am in no way an expert. Defining the GTD domain, which is to say, getting to clear nomenclature with clear actions, is still a work in progress. * Animated demos of org-gtd ** Projects [[doc/project.gif]] diff --git a/changes-for-3.0.org b/changes-for-3.0.org new file mode 100644 index 0000000..1d4fce0 --- /dev/null +++ b/changes-for-3.0.org @@ -0,0 +1,51 @@ +* minimum emacs requirement: 27.2 +* update your mind +** menu has changed +(a)rchive is now (k)nowledge +(m)odify project is now (a)dd to project +** org-gtd-delegate is now org-gtd-delegate-item-at-point +** org-gtd-agenda-projectify is now just org-gtd-clarify-agenda-item +** org-gtd-agenda-delegate is now org-gtd-delegate-agenda-item +** org-gtd-cancel-project is now org-gtd-project-cancel +** org-gtd-agenda-cancel-project is now org-gtd-project-cancel-from-agenda +** org-gtd-show-stuck-projects is now org-gtd-review-stuck-projects +* new features +** org-gtd-oops command for missed appointments +** you can now clarify single items with org-gtd-clarify-item +** you can customize what the functions do for each GTD action (e.g. incubate, single action, etc.) +** organize-hooks can be customized to apply to specific types of tasks (see org-gtd-organize-type-member-p ) +** you can customize your TODO keywords +** areas of focus customizable variable and optional decoration hook +** horizons file customizable, can be displayed/toggled while clarifying items +suggest keybinding on keymap in doc +** project templates +** functions you can call to automatically add items to the GTD flow +(nothing for knowledge, projects, quick action, or trash) +sample hook +#+begin_src elisp + (defun org-gtd-delegate-from-email () + (let ((delegated-to (message-fetch-field "to")) + (topic (format "Check in on %s" (message-fetch-field "subject"))) + (checkin-date (format-time-string "%Y-%m-%d")) + (org-gtd-delegate-create topic delegated-to checkin-date))) +#+end_src +* required config changes +** org-gtd-process-mode is now org-gtd-clarify-mode +** org-gtd-process-map is now org-gtd-clarify-map +** org-gtd-choose is now org-gtd-organize +** org-gtd-capture config is now org-capture config, not the 2.0 crippled one +** drop the headers +point people to org documentation for startup / variables to determine logging behavior if they want to keep it +-> logdone logrepeat logreschedule logredeadline +https://orgmode.org/manual/In_002dbuffer-Settings.html +** change org-edna triggers again +** calendar items no longer use SCHEDULED +** hook name has changed +org-gtd-process-item-hooks -> org-gtd-organize-hooks +* implementation +| x | delegate | WAIT | ORG_GTD_TIMESTAMP, DELEGATED_TO | actions | +| x | single action | NEXT | N/A | actions | +| x | calendar | N/A | ORG_GTD_TIMESTAMP | calendar | +| x | habit | NEXT, TODO | STYLE=habit, SCHEDULED | habits | +| x | incubate | N/A | ORG_GTD_TIMESTAMP | incubated | +| x | projects | NEXT, TODO | N/A | projects | diff --git a/dev/.emacs b/dev/.emacs index 6f3f398..d584abc 100644 --- a/dev/.emacs +++ b/dev/.emacs @@ -18,12 +18,14 @@ (eval-print-last-sexp))) (load bootstrap-file nil 'nomessage)) +(straight-use-package 'org) + (require 'package) (package-initialize) (unless (package-installed-p 'quelpa) (with-temp-buffer - (url-insert-file-contents "https://raw.githubusercontent.com/quelpa/quelpa/master/quelpa.el") + (url-insert-file-contents "https://github.com/quelpa/quelpa/raw/master/quelpa.el") (eval-buffer) (quelpa-self-upgrade))) @@ -42,6 +44,7 @@ (assq-delete-all 'org package--builtin-versions) (straight-use-package '(org-gtd :type git :host github :repo "trevoke/org-gtd.el")) +(setq org-gtd-update-ack "2.1.0") (require 'org-gtd) ;; (use-package org @@ -86,8 +89,8 @@ (add-hook 'minibuffer-setup-hook 'my-minibuffer-setup) (defun my-minibuffer-setup () - (set (make-local-variable 'face-remapping-alist) - '((default :height 2.0)))) + (set (make-local-variable 'face-remapping-alist) + '((default :height 2.0)))) (custom-set-variables ;; custom-set-variables was added by Custom. diff --git a/doc/org-gtd.org b/doc/org-gtd.org index d97917b..a03bdc1 100644 --- a/doc/org-gtd.org +++ b/doc/org-gtd.org @@ -325,7 +325,7 @@ Make sure you also read about sub-package configuration: [[*Required configurati There's an important keymap you'll want to make the flow of processing the inbox smoother. To limit impact on your emacs configuration, there is a specific keymap you can use. The function you'll want to bind is ~org-gtd-choose~. I suggest ~C-c c~, as in the following example. #+begin_src elisp -(define-key org-gtd-process-map (kbd "C-c c") #'org-gtd-choose) +(define-key org-gtd-clarify-map (kbd "C-c c") #'org-gtd-organize) #+end_src For other keybindings, do what you need. My bindings use ~C-c d~ as a prefix, i.e.: @@ -334,7 +334,25 @@ For other keybindings, do what you need. My bindings use ~C-c d~ as a prefix, i. - ~C-c d e~ :: ~org-gtd-engage~ etc. - +*** Sample Doom Emacs Config +If you are a Doom Emacs user, then your configuration may look something like this: + +#+BEGIN_SRC elisp +(use-package! org-gtd + :after org + :config + (org-edna-mode) + (setq org-edna-use-inheritance t) + (map! :leader + (:prefix ("d" . "org-gtd") + :desc "Capture" "c" #'org-gtd-capture + :desc "Engage" "e" #'org-gtd-engage + :desc "Process inbox" "p" #'org-gtd-process-inbox + :desc "Show all next" "n" #'org-gtd-show-all-next + :desc "Stuck projects" "s" #'org-gtd-show-stuck-projects)) + (map! :map org-gtd-process-map + :desc "Choose" "C-c c" #'org-gtd-choose)) +#+END_SRC * Using Org GTD :PROPERTIES: :DESCRIPTION: How Org GTD maps to the GTD flow diff --git a/doc/org-gtd.texi b/doc/org-gtd.texi index df8926f..b56c62e 100644 --- a/doc/org-gtd.texi +++ b/doc/org-gtd.texi @@ -82,6 +82,7 @@ Configuring * Required configuration of sub-packages:: * configuration options for org-gtd:: * Recommended key bindings:: +* Sample Doom Emacs Config:: Using Org GTD @@ -457,6 +458,7 @@ Finally, add this to your config: * Required configuration of sub-packages:: * configuration options for org-gtd:: * Recommended key bindings:: +* Sample Doom Emacs Config:: @end menu @node The easy way @@ -548,6 +550,28 @@ For other keybindings, do what you need. My bindings use @code{C-c d} as a prefi etc. +@node Sample Doom Emacs Config +@subsection Sample Doom Emacs Config + +If you are a Doom Emacs user, then your configuration may look something like this: + +@lisp +(use-package! org-gtd + :after org + :config + (org-edna-mode) + (setq org-edna-use-inheritance t) + (map! :leader + (:prefix ("d" . "org-gtd") + :desc "Capture" "c" #'org-gtd-capture + :desc "Engage" "e" #'org-gtd-engage + :desc "Process inbox" "p" #'org-gtd-process-inbox + :desc "Show all next" "n" #'org-gtd-show-all-next + :desc "Stuck projects" "s" #'org-gtd-show-stuck-projects)) + (map! :map org-gtd-process-map + :desc "Choose" "C-c c" #'org-gtd-choose)) +@end lisp + @node Using Org GTD @chapter Using Org GTD diff --git a/org-gtd-agenda.el b/org-gtd-agenda.el index d50cdac..5b30713 100644 --- a/org-gtd-agenda.el +++ b/org-gtd-agenda.el @@ -26,15 +26,48 @@ (require 'winner) (require 'org-agenda) -(require 'org-gtd-customize) + (require 'org-gtd-core) +(require 'org-gtd-backward-compatibility) + +(defgroup org-gtd-agenda nil + "Options for org-gtd agenda views." + :package-version '(org-gtd . "3.0.0") + :group 'org-gtd) + +(defcustom org-gtd-agenda-custom-commands + `(("g" "Scheduled today and all NEXT items" + ( + (agenda "" ((org-agenda-span 1) + (org-agenda-start-day nil) + (org-agenda-skip-additional-timestamps-same-entry t))) + (todo org-gtd-next ((org-agenda-overriding-header "All actions ready to be executed.") + (org-agenda-prefix-format '((todo . " %i %-12:(org-gtd-agenda--prefix-format)"))))) + (todo org-gtd-wait ((org-agenda-todo-ignore-with-date t) + (org-agenda-overriding-header "Delegated/Blocked items") + (org-agenda-prefix-format '((todo . " %i %-12 (org-gtd-agenda--prefix-format)")))))))) + "Agenda custom commands to be used for org-gtd. + +The provided default is to show the agenda for today and all TODOs marked as +`org-gtd-next' or `org-gtd-wait'. See documentation for +`org-agenda-custom-commands' to customize this further. + +NOTE! The function `org-gtd-engage' assumes the 'g' shortcut exists. +It's recommended you add to this list without modifying this first entry. You +can leverage this customization feature with command `org-gtd-mode' +or by wrapping your own custom functions with `with-org-gtd-context'." + :group 'org-gtd-agenda + :type 'sexp + :package-version '(org-gtd . "2.0.0")) ;;;###autoload (defun org-gtd-engage () "Display `org-agenda' customized by org-gtd." (interactive) + (org-gtd-core-prepare-agenda-buffers) (with-org-gtd-context - (org-agenda nil "g"))) + (org-agenda nil "g") + (goto-char (point-min)))) ;;;###autoload (defun org-gtd-show-all-next () @@ -42,76 +75,49 @@ This assumes all GTD files are also agenda files." (interactive) (with-org-gtd-context - (org-todo-list "NEXT"))) + (org-gtd-core-prepare-agenda-buffers) + (org-todo-list org-gtd-next))) ;;;###autoload -(defun org-gtd-agenda-projectify () - "Transform the current agenda item into a gtd project. - -This function is intended to be used on incubated items that come up." +(defun org-gtd-engage-grouped-by-context () + "Show all `org-gtd-next' actions grouped by context (tag prefixed with @)." (interactive) - (org-agenda-check-type t 'agenda 'todo 'tags 'search) - (org-agenda-check-no-diary) - (org-agenda-maybe-loop - #'org-gtd-agenda-projectify nil t nil - (let* ((marker (or (org-get-at-bol 'org-marker) - (org-agenda-error))) - (buffer (marker-buffer marker)) - (pos (marker-position marker))) - (set-marker-insertion-type marker t) - (org-with-remote-undo buffer - (with-current-buffer buffer - (widen) - (goto-char pos) - (org-up-element) - (org-narrow-to-element) - (org-show-subtree) - (display-buffer-same-window buffer '()) - (org-gtd--project) - (widen) - (winner-undo)) - (org-agenda-show-tags))))) + (with-org-gtd-context + (let* ((contexts (seq-map + (lambda (x) (substring-no-properties x)) + (seq-uniq + (flatten-list + (org-map-entries + (lambda () org-scanner-tags) + (format "{^@}+TODO=\"%s\"" org-gtd-next) + 'agenda))))) + (blocks (seq-map + (lambda (x) `(tags ,(format "+%s+TODO=\"%s\"" x org-gtd-next) + ((org-agenda-overriding-header ,x)))) + contexts)) + (org-agenda-custom-commands `(("g" "actions by context" ,blocks)))) + (org-agenda nil "g")))) -;;;###autoload -(defun org-gtd-agenda-delegate () - "Delegate current agenda task." - (interactive) - (org-agenda-check-type t 'agenda 'todo 'tags 'search) - (org-agenda-check-no-diary) - (org-agenda-maybe-loop - #'org-gtd-agenda-delegate nil t nil - (let* ((marker (or (org-get-at-bol 'org-marker) - (org-agenda-error))) - (buffer (marker-buffer marker)) - (pos (marker-position marker))) - (set-marker-insertion-type marker t) - (org-with-remote-undo buffer - (with-current-buffer buffer - (widen) - (goto-char pos) - (org-gtd-delegate)) - (org-agenda-show-tags))))) +(defun org-gtd-agenda--prefix-format () + "Format prefix for items in agenda buffer." + (let* ((elt (org-element-at-point)) + (level (org-element-property :level elt)) + (category (org-entry-get (point) "CATEGORY" t)) + (parent-title (org-element-property + :raw-value + (org-element-property :parent elt))) + (tally-cookie-regexp "\[[[:digit:]]+/[[:digit:]]+\][[:space:]]*")) -;;;###autoload -(defun org-gtd-agenda-cancel-project () - "Cancel the project that has the highlighted task." - (interactive) - (org-agenda-check-type t 'agenda 'todo 'tags 'search) - (org-agenda-check-no-diary) - (org-agenda-maybe-loop - #'org-gtd-agenda-cancel-project nil t nil - (let* ((marker (or (org-get-at-bol 'org-marker) - (org-agenda-error))) - (buffer (marker-buffer marker)) - (pos (marker-position marker))) - (set-marker-insertion-type marker t) - (org-with-remote-undo buffer - (with-current-buffer buffer - (widen) - (goto-char pos) - (org-up-heading-safe) - (org-gtd-cancel-project)) - (org-agenda-show-tags))))) + (cond + ((eq level 3) + (concat + (substring (string-pad + (replace-regexp-in-string tally-cookie-regexp "" parent-title) + 11) + 0 10) + "…")) + (category (concat (substring (string-pad category 11) 0 10) "…")) + (t "Simple task")))) (provide 'org-gtd-agenda) ;;; org-gtd-agenda.el ends here diff --git a/org-gtd-archive.el b/org-gtd-archive.el index 977ff1e..084d119 100644 --- a/org-gtd-archive.el +++ b/org-gtd-archive.el @@ -24,31 +24,79 @@ ;; ;;; Code: +(require 'f) (require 'org-archive) (require 'org-element) (require 'org-gtd-core) (require 'org-gtd-agenda) +(defcustom org-gtd-archive-location + #'org-gtd-archive-location-func + "Function to generate archive location for org gtd. + +That is to say, when items get cleaned up from the active files, they will go +to whatever file/tree is generated by this function. See `org-archive-location' +to learn more about the valid values generated. Note that this will only be +the file used by the standard `org-archive' functions if you +enable command `org-gtd-mode'. If not, this will be used only by +org-gtd's archive behavior. + +This function has an arity of zero. By default this generates a file +called gtd_archive_ in `org-gtd-directory' and puts the entries +into a datetree." + :group 'org-gtd + :type 'sexp + :package-version '(org-gtd . "2.0.0")) + +(defconst org-gtd-archive-file-format "gtd_archive_%s" + "File name format for where org-gtd archives things by default.") + +(defun org-gtd-archive-item-at-point () + "Dirty hack to force archiving where I know I can." + (interactive) + (let* ((last-command nil) + (temp-file (make-temp-file org-gtd-directory nil ".org")) + (buffer (find-file-noselect temp-file))) + (org-copy-subtree) + (org-gtd-core-prepare-buffer buffer) + (with-current-buffer buffer + (org-paste-subtree) + (goto-char (point-min)) + (with-org-gtd-context (org-archive-subtree)) + (basic-save-buffer) + (kill-buffer)) + (delete-file temp-file))) + +(defun org-gtd-archive-location-func () + "Default function to define where to archive items." + (let* ((year (number-to-string (caddr (calendar-current-date)))) + (full-org-gtd-path (expand-file-name org-gtd-directory)) + (filename (format org-gtd-archive-file-format year)) + (filepath (f-join full-org-gtd-path filename))) + (string-join `(,filepath "::" "datetree/")))) + ;;;###autoload (defun org-gtd-archive-completed-items () "Archive everything that needs to be archived in your org-gtd." (interactive) + (org-gtd-core-prepare-agenda-buffers) (with-org-gtd-context - (org-gtd--archive-complete-projects) - (org-map-entries #'org-gtd--archive-completed-actions - "+LEVEL=2&+ORG_GTD=\"Actions\"" - 'agenda) - (org-map-entries #'org-gtd--archive-completed-actions - "+LEVEL=2&+ORG_GTD=\"Calendar\"" - 'agenda) - (org-map-entries #'org-gtd--archive-completed-actions - "+LEVEL=2&+ORG_GTD=\"Incubated\"" - 'agenda))) + (org-gtd--archive-complete-projects) + (org-map-entries #'org-gtd--archive-completed-actions + "+LEVEL=2&+ORG_GTD=\"Actions\"" + 'agenda) + (org-map-entries #'org-gtd--archive-completed-actions + "+LEVEL=2&+ORG_GTD=\"Calendar\"" + 'agenda) + (org-map-entries #'org-gtd--archive-completed-actions + "+LEVEL=2&+ORG_GTD=\"Incubated\"" + 'agenda))) (defun org-gtd--archive-complete-projects () "Archive all projects for which all actions/tasks are marked as done. -Done here is any done `org-todo-keyword'. For org-gtd this means DONE or CNCL." +Done here is any done `org-todo-keyword'. For org-gtd this means `org-gtd-done' +or `org-gtd-canceled'." (org-map-entries (lambda () @@ -61,7 +109,7 @@ Done here is any done `org-todo-keyword'. For org-gtd this means DONE or CNCL." 'agenda)) (defun org-gtd--all-subheadings-in-done-type-p () - "Private function. Returns t if every sub-heading is in a DONE or CNCL state." + "Return t if every sub-heading is `org-gtd-done' or `org-gtd-canceled'." (seq-every-p (lambda (x) (eq x 'done)) (org-map-entries (lambda () (org-element-property :todo-type (org-element-at-point))) diff --git a/org-gtd-areas-of-focus.el b/org-gtd-areas-of-focus.el new file mode 100644 index 0000000..c0ba1bf --- /dev/null +++ b/org-gtd-areas-of-focus.el @@ -0,0 +1,56 @@ +;;; org-gtd-areas-of-focus.el --- Areas of Focus for org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Areas of Focus are horizon 2 for GTD. +;; This logic helps handle them. +;; +;;; Code: + +(require 'org) + +(require 'org-gtd-core) +(require 'org-gtd-organize) + +(defcustom org-gtd-areas-of-focus '("Home" "Health" "Family" "Career") + "The current major areas in your life where you don't want to drop balls." + :type 'list + :group 'org-gtd + :package-version '(org-gtd . "3.0.0")) + +(defun org-gtd-areas-of-focus--set () + "Use as a hook when decorating items after clarifying them. + +This function requires that the user input find a match amongst the options. +If a new area of focus pops up for you, change the value of the eponymous +variable." + (unless (org-gtd-organize-type-member-p '(project-task trash knowledge quick-action)) + (let ((chosen-area (completing-read + "Which area of focus does this belong to? " + org-gtd-areas-of-focus + nil + t))) + (org-entry-put (point) "CATEGORY" chosen-area)))) + +(defalias 'org-gtd-set-area-of-focus 'org-gtd-areas-of-focus--set) + +(provide 'org-gtd-areas-of-focus) +;;; org-gtd-areas-of-focus.el ends here diff --git a/org-gtd-backward-compatibility.el b/org-gtd-backward-compatibility.el new file mode 100644 index 0000000..8778b54 --- /dev/null +++ b/org-gtd-backward-compatibility.el @@ -0,0 +1,63 @@ +;;; org-gtd-backward-compatibility.el --- Functions added in later versions of emacs -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Functions that don't exist in older vanilla emacsen +;; +;;; Code: +(require 'subr-x) + +(defun org-gtd--string-pad (string length &optional padding start) + "Pad STRING to LENGTH using PADDING. +If PADDING is nil, the space character is used. If not nil, it +should be a character. + +If STRING is longer than the absolute value of LENGTH, no padding +is done. + +If START is nil (or not present), the padding is done to the end +of the string, and if non-nil, padding is done to the start of +the string." + (unless (natnump length) + (signal 'wrong-type-argument (list 'natnump length))) + (let ((pad-length (- length (length string)))) + (cond ((<= pad-length 0) string) + (start (concat (make-string pad-length (or padding ?\s)) string)) + (t (concat string (make-string pad-length (or padding ?\s))))))) + +;; this was added in emacs 28.1 +(unless (fboundp 'string-pad) + (defalias 'string-pad 'org-gtd--string-pad)) + +(defun org-gtd--ensure-list (object) + "Return OBJECT as a list. +If OBJECT is already a list, return OBJECT itself. If it's +not a list, return a one-element list containing OBJECT." + (if (listp object) + object + (list object))) + +;; this was added in emacs 28.1 +(unless (fboundp 'ensure-list) + (defalias 'ensure-list 'org-gtd--ensure-list)) + +(provide 'org-gtd-backward-compatibility) +;;; org-gtd-backward-compatibility.el ends here diff --git a/org-gtd-calendar.el b/org-gtd-calendar.el new file mode 100644 index 0000000..4b5ff63 --- /dev/null +++ b/org-gtd-calendar.el @@ -0,0 +1,88 @@ +;;; org-gtd-calendar.el --- Define calendar items in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Calendar items have their own state and logic, defined here. +;; +;;; Code: + +(defconst org-gtd-calendar "Calendar") + +(defconst org-gtd-calendar-template + (format "* Calendar +:PROPERTIES: +:ORG_GTD: %s +:END: +" org-gtd-calendar)) + +(defcustom org-gtd-calendar-func + #'org-gtd-calendar--apply + "Function called when item at point is a task that must happen on a given day. + +Keep this clean and don't load your calendar with things that aren't +actually appointments or deadlines." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +;;;###autoload +(defun org-gtd-calendar (&optional appointment-date) + "Decorate and refile item at point as a calendar item. + +You can pass APPOINTMENT-DATE as a YYYY-MM-DD string if you want to use this +non-interactively." + (interactive) + (org-gtd-organize--call + (apply-partially org-gtd-calendar-func + appointment-date))) + +(defun org-gtd-calendar--apply (&optional appointment-date) + "Add a date/time to this item and store in org gtd. + +You can pass APPOINTMENT-DATE as a YYYY-MM-DD string if you want to use this +non-interactively." + (let ((date (or appointment-date + (org-read-date t nil nil "When is this going to happen? ")))) + (org-entry-put (point) org-gtd-timestamp (format "<%s>" date)) + (save-excursion + (org-end-of-meta-data t) + (open-line 1) + (insert (format "<%s>" date)))) + (setq-local org-gtd--organize-type 'calendar) + (org-gtd-organize-apply-hooks) + (org-gtd--refile org-gtd-calendar org-gtd-calendar-template)) + +(defun org-gtd-calendar-create (topic appointment-date) + "Automatically create a calendar task in the GTD flow. + +Takes TOPIC as the string from which to make the heading to add to `org-gtd' and +APPOINTMENT-DATE as a YYYY-MM-DD string." + (let ((buffer (generate-new-buffer "Org GTD programmatic temp buffer")) + (org-id-overriding-file-name "org-gtd")) + (with-current-buffer buffer + (org-mode) + (insert (format "* %s" topic)) + (org-gtd-clarify-item) + (org-gtd-calendar appointment-date)) + (kill-buffer buffer))) + +(provide 'org-gtd-calendar) +;;; org-gtd-calendar.el ends here diff --git a/org-gtd-capture.el b/org-gtd-capture.el index 69df782..34f1258 100644 --- a/org-gtd-capture.el +++ b/org-gtd-capture.el @@ -25,19 +25,60 @@ ;;; Code: (require 'org-capture) -(require 'org-gtd-core) + (require 'org-gtd-files) +(defconst org-gtd-inbox "inbox") + +(defconst org-gtd-inbox-template + "#+begin_comment +This is the inbox. Everything goes in here when you capture it. +#+end_comment +" + "Template for the GTD inbox.") + +(defcustom org-gtd-capture-templates + `(("i" "Inbox" + entry (file ,#'org-gtd-inbox-path) + "* %?\n%U\n\n %i" + :kill-buffer t) + ("l" "Inbox with link" + entry (file ,#'org-gtd-inbox-path) + "* %?\n%U\n\n %i\n %a" + :kill-buffer t)) + "Capture templates to be used when adding something to the inbox. + +See `org-capture-templates' for the format of each capture template. +Make the sure the template string starts with a single asterisk to denote a +top level heading, or the behavior of org-gtd will be undefined." + :group 'org-gtd + :type 'sexp + :package-version '(org-gtd . "2.0.0")) + +(defmacro with-org-gtd-capture (&rest body) + "Wrap BODY... with let-bound `org-gtd' variables for capture purposes." + (declare (debug t) (indent 2)) + `(let ((org-capture-templates org-gtd-capture-templates)) + (unwind-protect + (progn ,@body)))) + ;;;###autoload (defun org-gtd-capture (&optional goto keys) "Capture something into the GTD inbox. Wraps the function `org-capture' to ensure the inbox exists. - -For GOTO and KEYS, see `org-capture' documentation for the variables of the same name." +For GOTO and KEYS, see `org-capture' documentation for the variables of the +same name." (interactive) - (with-org-gtd-context + (with-org-gtd-capture (org-capture goto keys))) +;;;###autoload +(defun org-gtd-inbox-path () + "Return the full path to the inbox file." + (let ((path (org-gtd--path org-gtd-inbox))) + (org-gtd--ensure-file-exists path org-gtd-inbox-template) + path)) + (provide 'org-gtd-capture) ;;; org-gtd-capture.el ends here diff --git a/org-gtd-clarify.el b/org-gtd-clarify.el new file mode 100644 index 0000000..e66e91e --- /dev/null +++ b/org-gtd-clarify.el @@ -0,0 +1,219 @@ +;;; org-gtd-clarify.el --- Handle clarifying tasks -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Set up Emacs to helpfully clarify tasks so they can then be organized. +;; +;;; Code: + +(require 'org-agenda) + +(require 'org-gtd-id) +(require 'org-gtd-horizons) + +(defgroup org-gtd-clarify nil + "Customize the behavior when clarifying an item." + :package-version '(org-gtd . "3.0") + :group 'org-gtd) + +(defcustom org-gtd-clarify-show-horizons nil + "If t, show a side buffer with the higher horizons during item clarification. +The file shown can be configured in `org-gtd-horizons-file'" + :options '('right 'top 'left 'bottom 'nil) + :package-version '(org-gtd . "3.0") + :group 'org-gtd-clarify + :type 'symbol) + +(defcustom org-gtd-clarify-project-templates nil + "This is an alist of (\"template title\" . \"template\"). + +Used by `org-gtd-clarify-projects-insert-template', when clarifying an item +which turns out to be a project." + :group 'org-gtd-clarify + :type '(alist :key-type string :value-type string) + :package-version '(org-gtd . "3.0.0")) + +(defconst org-gtd-clarify--prefix "Org-GTD WIP") + +(defvar-local org-gtd-clarify--window-config nil + "Store window configuration prior to clarifying task") + +(defvar-local org-gtd-clarify--source-heading-marker nil + "Store marker to item that is being clarified") + +(defvar-local org-gtd-clarify--clarify-id nil + "Reference to the org id of the heading currently in the WIP buffer") + +(defvar-local org-gtd-clarify--inbox-p nil + "Used to separate a one-off clarify from the inbox clarification.") + +;;;###autoload +(defvar org-gtd-clarify-map (make-sparse-keymap) + "Keymap for command `org-gtd-clarify-mode', a minor mode.") + +;; code to make windows atomic, from emacs manual +;; (let ((window (split-window-right))) +;; (window-make-atom (window-parent window)) +;; (display-buffer-in-atom-window +;; (get-buffer-create "*Messages*") +;; `((window . ,(window-parent window)) (window-height . 5)))) + +;; code to make windows non-atomic +;; (walk-window-subtree (lambda (window) (set-window-parameter window 'window-atom nil)) (window-parent (get-buffer-window (current-buffer))) t) + +;; dedicated side window +;; (display-buffer-in-side-window (get-buffer "horizons.org") '((side . right) (dedicated . t))) + +;;;###autoload +(define-minor-mode org-gtd-clarify-mode + "Minor mode for org-gtd." + :lighter " GPM" + :keymap org-gtd-clarify-map + (if org-gtd-clarify-mode + (setq-local + header-line-format + (substitute-command-keys + "\\Clarify item. Use `\\[org-gtd-organize]' to file it appropriately when finished.")) + (setq-local header-line-format nil))) + +;;;###autoload +(defun org-gtd-clarify-item () + "Process item at point through org-gtd." + (declare (modes org-mode)) ;; for 27.2 compatibility + (interactive "i") + (let ((processing-buffer (org-gtd-clarify--get-buffer)) + (window-config (current-window-configuration)) + (source-heading-marker (point-marker))) + (org-gtd-clarify--maybe-initialize-buffer-contents processing-buffer) + (with-current-buffer processing-buffer + (setq-local org-gtd-clarify--window-config window-config + org-gtd-clarify--source-heading-marker source-heading-marker + org-gtd-clarify--clarify-id (org-id-get))) + (org-gtd-clarify-setup-windows processing-buffer))) + +(defun org-gtd-clarify--maybe-initialize-buffer-contents (buffer) + "If BUFFER is empty, then copy org heading at point and paste inside buffer." + (when (= (buffer-size buffer) 0) + (let ((last-command nil)) + (org-copy-subtree)) + (with-current-buffer buffer + (org-paste-subtree) + (org-entry-delete (point) org-gtd-timestamp) + (org-entry-delete (point) org-gtd-delegate-property) + (org-entry-delete (point) "STYLE")))) + +(defun org-gtd-clarify-inbox-item () + "Process item at point through org-gtd. + +This function is called through the inbox clarification process." + (org-gtd-clarify-item) + (setq-local org-gtd-clarify--inbox-p t)) + +(defun org-gtd-clarify-agenda-item () + "Process item at point on agenda view." + (declare (modes org-agenda-mode)) ;; for 27.2 compatibility + (interactive nil) + (org-agenda-check-type t 'agenda 'todo 'tags 'search) + (org-agenda-check-no-diary) + (let ((heading-marker (or (org-get-at-bol 'org-marker) + (org-agenda-error)))) + (org-gtd-clarify-item-at-marker heading-marker))) + +(defun org-gtd-clarify-item-at-marker (marker) + "MARKER must be a marker pointing to an org heading." + (let ((heading-buffer (marker-buffer marker)) + (heading-position (marker-position marker))) + (with-current-buffer heading-buffer + (goto-char heading-position) + (org-gtd-clarify-item)))) + +;;;###autoload +(defun org-gtd-clarify-switch-to-buffer () + "Prompt the user to choose one of the existing WIP buffers." + (interactive) + (let ((buf-names (mapcar #'buffer-name (org-gtd-clarify--get-buffers)))) + (if buf-names + (let ((chosen-buf-name (completing-read "Choose a buffer: " buf-names nil t))) + (org-gtd-clarify-setup-windows chosen-buf-name)) + (message "There are no Org-GTD WIP buffers.")))) + +(defun org-gtd-clarify-toggle-horizons-window () + "Toggle the window with the horizons buffer." + (interactive) + (let* ((buffer (org-gtd--horizons-file)) + (window (get-buffer-window buffer))) + (if window + (quit-window nil window) + (org-gtd-clarify--display-horizons-window)))) + +(defun org-gtd-clarify--display-horizons-window () + "Display horizons window." + (let ((horizons-side (or org-gtd-clarify-show-horizons 'right))) + (display-buffer (org-gtd--horizons-file) + `(display-buffer-in-side-window . ((side . ,horizons-side)))))) + +(defun org-gtd-clarify-project-insert-template () + "Insert user-provided template under item at point." + (let* ((choice (completing-read + "Choose a project template to insert: " + org-gtd-clarify-project-templates nil t)) + (chosen-template (alist-get + choice + org-gtd-clarify-project-templates nil nil 'equal))) + (save-excursion + (when (org-before-first-heading-p) + (org-next-visible-heading 1)) + (when (equal (point-min) (point)) + (goto-char 2)) + (org-paste-subtree 2 chosen-template)))) + +(defun org-gtd-clarify-setup-windows (buffer-or-name) + "Setup clarifying windows around BUFFER-OR-NAME." + (let ((buffer (get-buffer buffer-or-name))) + (set-buffer buffer) + (display-buffer buffer) + (delete-other-windows (get-buffer-window buffer)) + (if org-gtd-clarify-show-horizons + (org-gtd-clarify--display-horizons-window)))) + +(defun org-gtd-clarify--buffer-name (id) + "Retrieve the name of the WIP buffer for this particular ID." + (format "*%s: %s*" org-gtd-clarify--prefix id)) + +(defun org-gtd-clarify--get-buffers () + "Retrieve a list of Org GTD WIP buffers." + (seq-filter (lambda (buf) + (string-match-p org-gtd-clarify--prefix (buffer-name buf))) + (buffer-list))) + +(defun org-gtd-clarify--get-buffer () + "Get or create a WIP buffer for heading at point." + (org-gtd-id-get-create) + (let* ((org-id (org-gtd-id-get-create)) + (buffer (get-buffer-create (org-gtd-clarify--buffer-name org-id)))) + (with-current-buffer buffer + (unless (eq major-mode 'org-mode) (org-mode)) + (org-gtd-core-prepare-buffer) + (org-gtd-clarify-mode 1) + buffer))) + +(provide 'org-gtd-clarify) +;;; org-gtd-clarify.el ends here diff --git a/org-gtd-core.el b/org-gtd-core.el index 5ed7153..97f856b 100644 --- a/org-gtd-core.el +++ b/org-gtd-core.el @@ -26,49 +26,197 @@ ;;; Code: (require 'org-agenda-property) -(require 'org-capture) -(require 'org-gtd-customize) - -(defconst org-gtd-inbox "inbox") -(defconst org-gtd-incubated "incubated") -(defconst org-gtd-projects "projects") -(defconst org-gtd-actions "actions") -(defconst org-gtd-delegated "delegated") -(defconst org-gtd-calendar "calendar") - -(defconst org-gtd--properties - (let ((myhash (make-hash-table :test 'equal))) - (puthash org-gtd-actions "Actions" myhash) - (puthash org-gtd-incubated "Incubated" myhash) - (puthash org-gtd-projects "Projects" myhash) - (puthash org-gtd-calendar "Calendar" myhash) - myhash)) - -(defconst org-gtd-project-headings - "+LEVEL=2&+ORG_GTD=\"Projects\"" - "How to tell org-mode to find project headings") - -(defconst org-gtd-stuck-projects - `(,org-gtd-project-headings ("NEXT" "WAIT") nil "") - "How to identify stuck projects in the GTD system. - -This is a list of four items, the same type as in `org-stuck-projects'.") + +(require 'org-gtd-backward-compatibility) + +(defgroup org-gtd nil + "Customize the org-gtd package." + :link '(url-link "https://github.com/Trevoke/org-gtd.el") + :package-version '(org-gtd . "0.1") + :group 'org) + +(defcustom org-gtd-directory "~/gtd/" + "Directory for org-gtd. + +The package will use this directory for all its functionality, whether it is +building the agenda or refiling items. This is the directory where you will +find the default org-gtd file, and it is the directory where you should place +your own files if you want multiple refile targets (projects, etc.)." + :group 'org-gtd + :package-version '(org-gtd . "0.1") + :type 'directory) + +(defcustom org-gtd-next "NEXT" + "The `org-mode' keyword for an action ready to be done. Just the word." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-next-suffix "(n)" + "Additional `org-mode' tools for this keyword. Example: \"(w@/!)\". + +You can define: +- a key to be used with `org-use-fast-todo-selection' +- behavior (optional note/timestamp) for entering state +- behavior (optional note/timestamp) for leaving state. + +See `org-todo-keywords' for definition." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-todo "TODO" + "The `org-mode' keyword for an upcoming action (not yet ready, not blocked). + +See `org-todo-keywords' for customization options." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-todo-suffix "(t)" + "Additional `org-mode' tools for this keyword. Example: \"(w@/!)\". + +You can define: +- a key to be used with `org-use-fast-todo-selection' +- behavior (optional note/timestamp) for entering state +- behavior (optional note/timestamp) for leaving state. + +See `org-todo-keywords' for definition." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-wait "WAIT" + "The `org-mode' keyword when an action is blocked/delegated. + +See `org-todo-keywords' for customization options." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-wait-suffix "(w@)" + "Additional `org-mode' tools for this keyword. Example: \"(w@/!)\". + +You can define: +- a key to be used with `org-use-fast-todo-selection' +- behavior (optional note/timestamp) for entering state +- behavior (optional note/timestamp) for leaving state. + +See `org-todo-keywords' for definition." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-done "DONE" + "The `org-mode' keyword for a finished task. + + See `org-todo-keywords' for customization options." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-done-suffix "(d)" + "Additional `org-mode' tools for this keyword. Example: \"(w@/!)\". + +You can define: +- a key to be used with `org-use-fast-todo-selection' +- behavior (optional note/timestamp) for entering state +- behavior (optional note/timestamp) for leaving state. + +See `org-todo-keywords' for definition." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-canceled "CNCL" + "The `org-mode' keyword for a canceled task. + + See `org-todo-keywords' for customization options." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defcustom org-gtd-canceled-suffix "(c@)" + "Additional `org-mode' tools for this keyword. Example: \"(w@/!)\". + +You can define: +- a key to be used with `org-use-fast-todo-selection' +- behavior (optional note/timestamp) for entering state +- behavior (optional note/timestamp) for leaving state. + +See `org-todo-keywords' for definition." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'string) + +(defconst org-gtd-timestamp "ORG_GTD_TIMESTAMP" + "Org property storing timestamps for `org-gtd' logic.") + +(defvar org-gtd-project-headings) +(defvar org-gtd-stuck-projects) +(defvar org-gtd-archive-location) + +(defvar-local org-gtd--loading-p nil + "`Org-gtd' sets this variable after it has changed the state in this buffer.") + +(define-error + 'org-gtd-error + "Something went wrong with `org-gtd'" + 'user-error) ;;;###autoload (defmacro with-org-gtd-context (&rest body) - "Wrap any BODY in this macro to inherit the org-gtd settings for your logic." + "Wrap BODY... in this macro to inherit the org-gtd settings for your logic." (declare (debug t) (indent 2)) `(let* ((org-use-property-inheritance "ORG_GTD") + (org-todo-keywords `((sequence ,(string-join `(,org-gtd-next ,org-gtd-next-suffix)) + ,(string-join `(,org-gtd-todo ,org-gtd-todo-suffix)) + ,(string-join `(,org-gtd-wait ,org-gtd-wait-suffix)) + "|" + ,(string-join `(,org-gtd-done ,org-gtd-done-suffix)) + ,(string-join `(,org-gtd-canceled ,org-gtd-canceled-suffix))))) + ;; (org-log-done 'time) + ;; (org-log-done-with-time t) + ;; (org-log-refile 'time) (org-archive-location (funcall org-gtd-archive-location)) - (org-capture-templates org-gtd-capture-templates) (org-refile-use-outline-path nil) (org-stuck-projects org-gtd-stuck-projects) (org-odd-levels-only nil) - (org-agenda-files `(,org-gtd-directory)) - (org-agenda-property-list '("DELEGATED_TO")) + (org-agenda-files (org-gtd-core--agenda-files)) + (org-agenda-property-list '(,org-gtd-delegate-property)) (org-agenda-custom-commands org-gtd-agenda-custom-commands)) (unwind-protect (progn ,@body)))) +(defun org-gtd-core-prepare-buffer (&optional buffer) + "Make sure BUFFER is prepared to handle Org GTD operations. + +If BUFFER is nil, use current buffer." + (with-current-buffer (or buffer (current-buffer)) + (unless (bound-and-true-p org-gtd--loading-p) + (setq-local org-gtd--loading-p t) + (with-org-gtd-context + (org-mode-restart)) + (setq-local org-gtd--loading-p t)))) + +(defun org-gtd-core-prepare-agenda-buffers () + "Ensure `org-mode' has the desired settings in the agenda buffers." + (mapc + (lambda (file) (org-gtd-core-prepare-buffer (find-file-noselect file))) + (-flatten + (mapcar + (lambda (org-agenda-entry) (if (f-directory-p org-agenda-entry) + (directory-files org-agenda-entry t org-agenda-file-regexp t) + org-agenda-entry)) + (with-org-gtd-context (org-gtd-core--agenda-files)))))) + +(defun org-gtd-core--agenda-files () + "Concatenate `org-agenda-files' variable with `org-gtd-directory' contents." + (if (stringp org-agenda-files) + (append (org-read-agenda-file-list) + (ensure-list org-gtd-directory)) + (append (ensure-list org-agenda-files) + (ensure-list org-gtd-directory)))) + (provide 'org-gtd-core) ;;; org-gtd-core.el ends here diff --git a/org-gtd-customize.el b/org-gtd-customize.el deleted file mode 100644 index eb7a527..0000000 --- a/org-gtd-customize.el +++ /dev/null @@ -1,185 +0,0 @@ -;;; org-gtd-customize.el --- Custom variables for org-gtd -*- lexical-binding: t; coding: utf-8 -*- -;; -;; Copyright © 2019-2023 Aldric Giacomoni - -;; Author: Aldric Giacomoni -;; This file is not part of GNU Emacs. - -;; This file 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, or (at your option) -;; any later version. - -;; This file 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 file. If not, see . - -;;; Commentary: -;; -;; User-customizable options for org-gtd. -;; -;;; Code: - -(require 'subr-x) - -(defgroup org-gtd nil - "Customize the org-gtd package." - :link '(url-link "https://github.com/Trevoke/org-gtd.el") - :package-version '(org-gtd . "0.1") - :group 'org) - -(defcustom org-gtd-directory "~/gtd/" - "Directory for org-gtd. - -The package will use this directory for all its functionality, whether it is -building the agenda or refiling items. This is the directory where you will -find the default org-gtd file, and it is the directory where you should place -your own files if you want multiple refile targets (projects, etc.)." - :group 'org-gtd - :package-version '(org-gtd . "0.1") - :type 'directory) - -(defcustom org-gtd-process-item-hooks '(org-set-tags-command) - "Enhancements to add to each item as they get processed from the inbox. - -This is a list of functions that modify an org element. The default value has -one function: setting org tags on the item. Some built-in examples are -provided as options here. You can create your own functions to enhance/decorate -the items once they have been processed and add them to that list." - :group 'org-gtd - :package-version '(org-gtd . "1.0.4") - :type 'hook - :options '(org-set-tags-command org-set-effort org-priority)) - -(defcustom org-gtd-archive-location - (lambda () - (let ((year (number-to-string (caddr (calendar-current-date))))) - (string-join `("gtd_archive_" ,year "::datetree/")))) - "Function to generate archive location for org gtd. - -That is to say, when items get cleaned up from the active files, they will go -to whatever file/tree is generated by this function. See `org-archive-location' -to learn more about the valid values generated. Note that this will only be -the file used by the standard `org-archive' functions if you -enable command `org-gtd-mode'. If not, this will be used only by -org-gtd's archive behavior. - -This function has an arity of zero. By default this generates a file -called gtd_archive_ in `org-gtd-directory' and puts the entries -into a datetree." - :group 'org-gtd - :type 'sexp - :package-version '(org-gtd . "2.0.0")) - -(defcustom org-gtd-capture-templates - `(("i" "Inbox" - entry (file ,#'org-gtd-inbox-path) - "* %?\n%U\n\n %i" - :kill-buffer t) - ("l" "Inbox with link" - entry (file ,#'org-gtd-inbox-path) - "* %?\n%U\n\n %i\n %a" - :kill-buffer t)) - "Capture templates to be used when adding something to the inbox. - -See `org-capture-templates' for the format of each capture template. -Make the sure the template string starts with a single asterisk to denote a -top level heading, or the behavior of org-gtd will be undefined." - :group 'org-gtd - :type 'sexp - :package-version '(org-gtd . "2.0.0")) - -(defcustom org-gtd-agenda-custom-commands - '(("g" "Scheduled today and all NEXT items" - ( - (agenda "" ((org-agenda-span 1) - (org-agenda-start-day nil))) - (todo "NEXT" ((org-agenda-overriding-header "All NEXT items") - (org-agenda-prefix-format '((todo . " %i %-12:(org-gtd--agenda-prefix-format)"))))) - (todo "WAIT" ((org-agenda-todo-ignore-with-date t) - (org-agenda-overriding-header "Delegated/Blocked items") - (org-agenda-prefix-format '((todo . " %i %-12 (org-gtd--agenda-prefix-format)")))))))) - "Agenda custom commands to be used for org-gtd. - -The provided default is to show the agenda for today and all TODOs marked as -NEXT or WAIT. See documentation for `org-agenda-custom-commands' to customize -this further. - -NOTE! The function `org-gtd-engage' assumes the 'g' shortcut exists. -It's recommended you add to this list without modifying this first entry. You -can leverage this customization feature with command `org-gtd-mode' -or by wrapping your own custom functions with `with-org-gtd-context'." - :group 'org-gtd - :type 'sexp - :package-version '(org-gtd . "2.0.0")) - -(defcustom org-gtd-refile-to-any-target t - "Set to true if you do not need to choose where to refile processed items. - -When this is true, org-gtd will refile to the first target it finds, or creates -if necessary, without confirmation. When this is false, it will ask for -confirmation regardless of the number of options. Note that setting this to -false does not mean you can safely create new targets. See the documentation -to create new refile targets. - -Defaults to true to carry over pre-2.0 behavior. You will need to change this -setting if you follow the instructions to add your own refile targets." - :group 'org-gtd - :type 'boolean - :package-version '(org-gtd . "2.0.0")) - -;; this was added in emacs 28.1 -(unless (fboundp 'string-pad) - (defun string-pad (string length &optional padding start) - "Pad STRING to LENGTH using PADDING. -If PADDING is nil, the space character is used. If not nil, it -should be a character. - -If STRING is longer than the absolute value of LENGTH, no padding -is done. - -If START is nil (or not present), the padding is done to the end -of the string, and if non-nil, padding is done to the start of -the string." - (unless (natnump length) - (signal 'wrong-type-argument (list 'natnump length))) - (let ((pad-length (- length (length string)))) - (cond ((<= pad-length 0) string) - (start (concat (make-string pad-length (or padding ?\s)) string)) - (t (concat string (make-string pad-length (or padding ?\s)))))))) - - - -(defun org-gtd--agenda-prefix-format () - "format prefix for items in buffer" - (let* ((elt (org-element-at-point)) - (level (org-element-property :level elt)) - (category (org-entry-get (point) "CATEGORY" t)) - (parent-title (org-element-property :raw-value (org-element-property :parent elt)))) - - (cond - ((eq level 3) (concat - (substring (string-pad (replace-regexp-in-string - "\[[[:digit:]]+/[[:digit:]]+\][[:space:]]*" - "" - parent-title) - 11) - 0 10) - "…")) - (category (concat (substring (string-pad category 11) 0 10) "…")) - "Simple task"))) - -(defcustom org-gtd-delegate-read-func (lambda () (read-string "Who will do this? ")) - "Function that is called to read in the Person the task is delegated to. - -Needs to return a string that will be used as the persons name." - :group 'org-gtd - :package-version '(org-gtd . "2.3.0") - :type 'function ) - -(provide 'org-gtd-customize) -;;; org-gtd-customize.el ends here diff --git a/org-gtd-delegate.el b/org-gtd-delegate.el index 9833366..501bc27 100644 --- a/org-gtd-delegate.el +++ b/org-gtd-delegate.el @@ -25,20 +25,107 @@ ;;; Code: (require 'org) -(require 'org-gtd-customize) + +(require 'org-gtd-single-action) + +(defconst org-gtd-delegate-property "DELEGATED_TO") + +(defcustom org-gtd-delegate-read-func (lambda () (read-string "Who will do this? ")) + "Function that is called to read in the Person the task is delegated to. + +Needs to return a string that will be used as the persons name." + :group 'org-gtd + :package-version '(org-gtd . "2.3.0") + :type 'function ) + +(defcustom org-gtd-organize-delegate-func + #'org-gtd-delegate--apply + "Function called when item at at point is an action delegated to someone else." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) ;;;###autoload -(defun org-gtd-delegate () - "Delegate item at point." +(defun org-gtd-delegate (&optional delegated-to checkin-date) + "Organize and refile item at point as a delegated item. + +You can pass DELEGATED-TO as the name of the person to whom this was delegated +and CHECKIN-DATE as the YYYY-MM-DD string of when you want `org-gtd' to remind +you if you want to call this non-interactively." (interactive) - (let ((delegated-to (apply org-gtd-delegate-read-func nil)) + (org-gtd-organize--call + (apply-partially org-gtd-organize-delegate-func + delegated-to + checkin-date))) + +(defun org-gtd-delegate--apply (&optional delegated-to checkin-date) + "Organize and refile this as a delegated item in the `org-gtd' system. + +You can pass DELEGATED-TO as the name of the person to whom this was delegated +and CHECKIN-DATE as the YYYY-MM-DD string of when you want `org-gtd' to remind +you if you want to call this non-interactively." + (org-gtd-delegate-item-at-point delegated-to checkin-date) + (setq-local org-gtd--organize-type 'delegated) + (org-gtd-organize-apply-hooks) + (org-gtd--refile org-gtd-action org-gtd-action-template)) + +;;;###autoload +(defun org-gtd-delegate-item-at-point (&optional delegated-to checkin-date) + "Delegate item at point. Use this if you do not want to refile the item. + +You can pass DELEGATED-TO as the name of the person to whom this was delegated +and CHECKIN-DATE as the YYYY-MM-DD string of when you want `org-gtd' to remind +you if you want to call this non-interactively. +If you call this interactively, the function will ask for the name of the +person to whom to delegate by using `org-gtd-delegate-read-func'." + (declare (modes org-mode)) ;; for 27.2 compatibility + (interactive "i") + (let ((delegated-to (or delegated-to + (apply org-gtd-delegate-read-func nil))) + (date (or checkin-date + (org-read-date t nil nil "When do you want to check in on this task? "))) (org-inhibit-logging 'note)) - (org-set-property "DELEGATED_TO" delegated-to) - (org-todo "WAIT") - (org-schedule 0) + (org-set-property org-gtd-delegate-property delegated-to) + (org-entry-put (point) org-gtd-timestamp (format "<%s>" date)) + (save-excursion + (org-end-of-meta-data t) + (open-line 1) + (insert (format "<%s>" date))) + (org-todo org-gtd-wait) (save-excursion (goto-char (org-log-beginning t)) (insert (format "programmatically delegated to %s\n" delegated-to))))) +;;;###autoload +(defun org-gtd-delegate-agenda-item () + "Delegate item at point on agenda view." + (declare (modes org-agenda-mode)) ;; for 27.2 compatibility + (interactive "i") + (org-agenda-check-type t 'agenda 'todo 'tags 'search) + (org-agenda-check-no-diary) + (let* ((heading-marker (or (org-get-at-bol 'org-marker) + (org-agenda-error))) + (heading-buffer (marker-buffer heading-marker)) + (heading-position (marker-position heading-marker))) + (with-current-buffer heading-buffer + (goto-char heading-position) + (org-gtd-delegate-item-at-point)))) + +(defun org-gtd-delegate-create (topic delegated-to checkin-date) + "Automatically create a delegated task in the GTD flow. + +TOPIC is the string you want to see in the agenda when this comes up. +DELEGATED-TO is the name of the person to whom this was delegated. +CHECKIN-DATE is the YYYY-MM-DD string of when you want `org-gtd' to remind +you." + (let ((buffer (generate-new-buffer "Org GTD programmatic temp buffer")) + (org-id-overriding-file-name "org-gtd")) + (with-current-buffer buffer + (org-mode) + (insert (format "* %s" topic)) + (org-gtd-clarify-item) + (org-gtd-delegate delegated-to checkin-date)) + (kill-buffer buffer))) + (provide 'org-gtd-delegate) ;;; org-gtd-delegate.el ends here diff --git a/org-gtd-files.el b/org-gtd-files.el index bcfddb5..ae7f68c 100644 --- a/org-gtd-files.el +++ b/org-gtd-files.el @@ -27,46 +27,20 @@ (require 'f) (require 'org-gtd-core) -(defconst org-gtd-inbox-template - "#+STARTUP: overview hidestars logrefile indent logdone -#+TODO: NEXT TODO WAIT | DONE CNCL TRASH -#+begin_comment -This is the inbox. Everything goes in here when you capture it. -#+end_comment -" - "Template for the GTD inbox.") - -(defconst org-gtd-file-header - "#+STARTUP: overview indent align inlineimages hidestars logdone logrepeat logreschedule logredeadline -#+TODO: NEXT(n) TODO(t) WAIT(w@) | DONE(d) CNCL(c@) -") - - (defconst org-gtd-default-file-name "org-gtd-tasks") -;;;###autoload -(defun org-gtd-inbox-path () - "Return the full path to the inbox file." - (let ((path (org-gtd--path org-gtd-inbox))) - (org-gtd--ensure-file-exists path org-gtd-inbox-template) - path)) - -(defun org-gtd--inbox-file () - "Create or return the buffer to the GTD inbox file." - (find-file-noselect (org-gtd-inbox-path))) - (defun org-gtd--default-file () "Create or return the buffer to the default GTD file." (let ((path (org-gtd--path org-gtd-default-file-name))) - (org-gtd--ensure-file-exists path org-gtd-file-header) + (org-gtd--ensure-file-exists path) (find-file-noselect path))) -(defun org-gtd--ensure-file-exists (path initial-contents) +(defun org-gtd--ensure-file-exists (path &optional initial-contents) "Create the file at PATH with INITIAL-CONTENTS if it does not exist." (unless (f-exists-p path) (with-current-buffer (find-file-noselect path) - (insert initial-contents) - (org-mode-restart) + (insert (or initial-contents "")) + (org-gtd-core-prepare-buffer) (basic-save-buffer)))) (defun org-gtd--path (file) diff --git a/org-gtd-habit.el b/org-gtd-habit.el new file mode 100644 index 0000000..4df6893 --- /dev/null +++ b/org-gtd-habit.el @@ -0,0 +1,86 @@ +;;; org-gtd-habit.el --- Define habits in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Habits have org-mode requirements, we satisfy them here. +;; +;;; Code: + +(defconst org-gtd-habit "Habits") + +(defconst org-gtd-habit-template + (format "* Habits +:PROPERTIES: +:ORG_GTD: %s +:END: +" org-gtd-habit)) + +(defcustom org-gtd-habit-func + #'org-gtd-habit--apply + "Function called when item at point is a habit." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +;;;###autoload +(defun org-gtd-habit (&optional repeater) + "Organize and refile item at point as a calendar item. + +If you want to call this non-interactively, +REPEATER is `org-mode'-style repeater string (.e.g \".+3d\") which will +determine how often you'll be reminded of this habit." + (interactive) + (org-gtd-organize--call + (apply-partially org-gtd-habit-func + repeater))) + +(defun org-gtd-habit--apply (&optional repeater) + "Add a repeater to this item and store in org gtd. + +If you want to call this non-interactively, +REPEATER is `org-mode'-style repeater string (.e.g \".+3d\") which will +determine how often you'll be reminded of this habit." + (let ((repeater (or repeater + (read-from-minibuffer "How do you want this to repeat? "))) + (today (format-time-string "%Y-%m-%d"))) + (org-schedule nil (format "<%s %s>" today repeater)) + (org-entry-put (point) "STYLE" "habit")) + (setq-local org-gtd--organize-type 'habit) + (org-gtd-organize-apply-hooks) + (org-gtd--refile org-gtd-habit org-gtd-habit-template)) + +(defun org-gtd-habit-create (topic repeater) + "Automatically create a habit in the GTD flow. + +TOPIC is the string you want to see in the `org-agenda' view. +REPEATER is `org-mode'-style repeater string (.e.g \".+3d\") which will +determine how often you'll be reminded of this habit." + (let ((buffer (generate-new-buffer "Org GTD programmatic temp buffer")) + (org-id-overriding-file-name "org-gtd")) + (with-current-buffer buffer + (org-mode) + (insert (format "* %s" topic)) + (org-gtd-clarify-item) + (org-gtd-habit repeater)) + (kill-buffer buffer))) + +(provide 'org-gtd-habit) +;;; org-gtd-habit.el ends here diff --git a/org-gtd-horizons.el b/org-gtd-horizons.el new file mode 100644 index 0000000..b4b358f --- /dev/null +++ b/org-gtd-horizons.el @@ -0,0 +1,54 @@ +;;; org-gtd-horizons.el --- manage the horizons buffer -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; The horizons help us ensure our lives are on the right track. +;; The buffer can show up when clarifying to make sure we don't +;; get distracted. +;; +;;; Code: + +(require 'org-gtd-files) + +(defcustom org-gtd-horizons-file "horizons.org" + "File holding your GTD horizons. + +This may get displayed during item clarification for context and focus. +This file must be in the `org-gtd-directory'." + :group 'org-gtd + :package-version '(org-gtd . "3.0") + :type 'file) + +(defconst org-gtd-file-horizons-template + "* Purpose and principles (why) +* Vision (what) +* Goals +* Areas of focus / accountabilities +") + +(defun org-gtd--horizons-file () + "Create or return the buffer to the file containing the GTD horizons." + (let ((path (f-join org-gtd-directory org-gtd-horizons-file))) + (org-gtd--ensure-file-exists path org-gtd-file-horizons-template) + (find-file-noselect path))) + +(provide 'org-gtd-horizons) +;;; org-gtd-horizons.el ends here diff --git a/org-gtd-id.el b/org-gtd-id.el new file mode 100644 index 0000000..0cd36de --- /dev/null +++ b/org-gtd-id.el @@ -0,0 +1,121 @@ +;;; org-gtd-id.el --- generating ids for tasks -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Generating ids from tasks. +;; Most of this code is stolen and adapted from Karl Voit's code and demo at +;; https://gitlab.com/publicvoit/orgmode-link-demo/-/raw/main/link_demo.org +;; +;;; Code: + +(require 'ffap) + +(defun org-gtd-id-get-create (&optional pom) + "Get the ID property of the entry at point-or-marker POM. +If POM is nil, refer to the entry at point. +If the entry does not have an ID, create an ID prefixed for org-gtd. +In any case, the ID of the entry is returned. + +This function is a modified copy of `org-id-get'." + (interactive) + (org-with-point-at pom + (let ((id (org-entry-get nil "ID"))) + (if (and id (stringp id) (string-match "\\S-" id)) + id + (setq id (org-gtd-id--generate)) + (org-entry-put pom "ID" id) + (org-id-add-location id (or org-id-overriding-file-name + (buffer-file-name (buffer-base-buffer)))) + id)))) + +(defun org-gtd-id--generate-sanitized-alnum-dash-string (str) + "Clean up STR and make it fit to be used as an org id. + +Returns a string which contains only a-zA-Z0-9 with single dashes replacing +all other characters in-between them. + +Some parts were copied and adapted from org-hugo-slug from +https://github.com/kaushalmodi/ox-hugo (GPLv3). + +Taken from +https://gitlab.com/publicvoit/orgmode-link-demo/-/raw/main/link_demo.org ." + (let* (;; Remove ".." HTML tags if present. + (str (replace-regexp-in-string "<\\(?1:[a-z]+\\)[^>]*>.*" "" str)) + ;; Remove URLs if present in the string. The ")" in the + ;; below regexp is the closing parenthesis of a Markdown + ;; link: [Desc](Link). + (str (replace-regexp-in-string (concat "\\](" ffap-url-regexp "[^)]+)") "]" str)) + ;; Replace "&" with " and ", "." with " dot ", "+" with + ;; " plus ". + (str (replace-regexp-in-string + "&" " and " + (replace-regexp-in-string + "\\." " dot " + (replace-regexp-in-string + "\\+" " plus " str)))) + ;; Replace German Umlauts with 7-bit ASCII. + (str (replace-regexp-in-string "ä" "ae" str nil)) + (str (replace-regexp-in-string "ü" "ue" str nil)) + (str (replace-regexp-in-string "ö" "oe" str nil)) + (str (replace-regexp-in-string "ß" "ss" str nil)) + ;; Replace all characters except alphabets, numbers and + ;; parentheses with spaces. + (str (replace-regexp-in-string "[^[:alnum:]()]" " " str)) + ;; On emacs 24.5, multibyte punctuation characters like ":" + ;; are considered as alphanumeric characters! Below evals to + ;; non-nil on emacs 24.5: + ;; (string-match-p "[[:alnum:]]+" ":") + ;; So replace them with space manually.. + (str (if (version< emacs-version "25.0") + (let ((multibyte-punctuations-str ":")) ;String of multibyte punctuation chars + (replace-regexp-in-string (format "[%s]" multibyte-punctuations-str) " " str)) + str)) + ;; Remove leading and trailing whitespace. + (str (replace-regexp-in-string "\\(^[[:space:]]*\\|[[:space:]]*$\\)" "" str)) + ;; Replace 2 or more spaces with a single space. + (str (replace-regexp-in-string "[[:space:]]\\{2,\\}" " " str)) + ;; Replace parentheses with double-hyphens. + (str (replace-regexp-in-string "\\s-*([[:space:]]*\\([^)]+?\\)[[:space:]]*)\\s-*" " -\\1- " str)) + ;; Remove any remaining parentheses character. + (str (replace-regexp-in-string "[()]" "" str)) + ;; Replace spaces with hyphens. + (str (replace-regexp-in-string " " "-" str)) + ;; Remove leading and trailing hyphens. + (str (replace-regexp-in-string "\\(^[-]*\\|[-]*$\\)" "" str))) + str)) + +(defun org-gtd-id--generate() + "Generate and return a new id. +The generated ID is stripped off potential progress indicator cookies and +sanitized to get a slug. Furthermore, it is suffixed with an ISO date-stamp." + (let* ((my-heading-text (nth 4 (org-heading-components))) ;; retrieve heading string + (my-heading-text (replace-regexp-in-string "\\(\\[[0-9]+%\\]\\)" "" my-heading-text)) ;; remove progress indicators like "[25%]" + (my-heading-text (replace-regexp-in-string "\\(\\[[0-9]+/[0-9]+\\]\\)" "" my-heading-text)) ;; remove progress indicators like "[2/7]" + (my-heading-text (replace-regexp-in-string "\\(\\[#[ABC]\\]\\)" "" my-heading-text)) ;; remove priority indicators like "[#A]" + (my-heading-text (replace-regexp-in-string "\\[\\[\\(.+?\\)\\]\\[" "" my-heading-text t)) ;; removes links, keeps their description and ending brackets + (my-heading-text (replace-regexp-in-string "<[12][0-9]\\{3\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\( .*?\\)>" "" my-heading-text t)) ;; removes day of week and time from active date- and time-stamps + (my-heading-text (replace-regexp-in-string "\\[[12][0-9]\\{3\\}-[0-9]\\{2\\}-[0-9]\\{2\\}\\( .*?\\)\\]" "" my-heading-text t)) ;; removes day of week and time from inactive date- and time-stamps + (raw-id (org-gtd-id--generate-sanitized-alnum-dash-string my-heading-text)) ;; get slug from heading text + (timestamp (format-time-string "%Y-%m-%d"))) + (concat raw-id "-" timestamp))) + +(provide 'org-gtd-id) +;;; org-gtd-id.el ends here diff --git a/org-gtd-inbox-processing.el b/org-gtd-inbox-processing.el deleted file mode 100644 index 20024ac..0000000 --- a/org-gtd-inbox-processing.el +++ /dev/null @@ -1,265 +0,0 @@ -;;; org-gtd-inbox-processing.el --- Code to process inbox -*- lexical-binding: t; coding: utf-8 -*- -;; -;; Copyright © 2019-2023 Aldric Giacomoni - -;; Author: Aldric Giacomoni -;; This file is not part of GNU Emacs. - -;; This file 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, or (at your option) -;; any later version. - -;; This file 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 file. If not, see . - -;;; Commentary: -;; -;; Inbox processing management for org-gtd. -;; -;;; Code: - -(require 'transient) -(require 'org-gtd-core) -(require 'org-gtd-agenda) -(require 'org-gtd-projects) -(require 'org-gtd-refile) - -;;;###autoload -(defvar org-gtd-process-map (make-sparse-keymap) - "Keymap for command `org-gtd-process-mode', a minor mode.") - -;;;###autoload -(define-minor-mode org-gtd-process-mode - "Minor mode for org-gtd." - :lighter " GPM" - :keymap org-gtd-process-map - (if org-gtd-process-mode - (setq-local - header-line-format - (substitute-command-keys - "\\Clarify item. Let Org GTD store it with `\\[org-gtd-choose]'.")) - (setq-local header-line-format nil))) - - -(transient-define-prefix org-gtd-choose () - "Choose how to categorize the current item. - -Note that this function is intended to be used only during inbox processing. -Each action continues inbox processing, so you may put your Emacs in an -undefined state." - ["Actionable" - [("q" "Quick action" org-gtd--quick-action) - ("s" "Single action" org-gtd--single-action)] - [("d" "Delegate" org-gtd--delegate) - ("c" "Calendar" org-gtd--calendar)] - [("p" "Project (multi-step)" org-gtd--project) - ("m" "Modify project: add this task" org-gtd--modify-project)] - ] - ["Non-actionable" - [("i" "Incubate" org-gtd--incubate) - ("a" "Archive this knowledge" org-gtd--archive)] - [("t" "Trash" org-gtd--trash)]] - ["Org GTD" - ("x" - "Exit. Stop processing the inbox for now." - org-gtd--stop-processing)]) - -;; something to uncomment and start using later. -;;;;###autoload -;; (defmacro org-gtd-organize-action (fun-name &rest body) -;; "Creates a function to hook into the transient for inbox item organization" -;; (declare (debug t) (indent defun)) -;; `(defun ,fun-name () -;; (interactive) -;; (unwind-protect (progn ,@body)) -;; (org-gtd-process-inbox))) - -;;;###autoload -(defun org-gtd-process-inbox () - "Process the GTD inbox." - (interactive) - (set-buffer (org-gtd--inbox-file)) - (display-buffer-same-window (current-buffer) '()) - (delete-other-windows) - - (org-gtd-process-mode) - - (condition-case err - (progn - (widen) - (goto-char (point-min)) - (org-next-visible-heading 1) - (org-back-to-heading) - (org-narrow-to-subtree)) - (user-error (org-gtd--stop-processing)))) - -;;;###autoload -(defun org-gtd--archive () - "Process GTD inbox item as a reference item." - (interactive) - (org-todo "DONE") - (with-org-gtd-context (org-archive-subtree)) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--project () - "Process GTD inbox item by transforming it into a project. -Allow the user apply user-defined tags from -`org-tag-persistent-alist', `org-tag-alist' or file-local tags in -the inbox. Refile to `org-gtd-actionable-file-basename'." - (interactive) - - (if (org-gtd--poorly-formatted-project-p) - (org-gtd--show-error-and-return-to-editing) - - (org-gtd--decorate-item) - (org-gtd-projects--nextify) - (goto-char (point-min)) - (let ((org-special-ctrl-a t)) - (org-end-of-line)) - (insert " [/]") - (org-update-statistics-cookies t) - (org-gtd--refile org-gtd-projects) - (org-gtd-process-inbox))) - -;;;###autoload -(defun org-gtd--modify-project () - "Refile the org heading at point under a chosen heading in the agenda files." - (interactive) - (with-org-gtd-context - (let* ((org-gtd-refile-to-any-target nil) - (org-use-property-inheritance '("ORG_GTD")) - (headings (org-map-entries - (lambda () (org-get-heading t t t t)) - org-gtd-project-headings - 'agenda)) - (chosen-heading (completing-read "Choose a heading: " headings nil t)) - (heading-marker (org-find-exact-heading-in-directory chosen-heading org-gtd-directory))) - (org-refile nil nil `(,chosen-heading - ,(buffer-file-name (marker-buffer heading-marker)) - nil - ,(marker-position heading-marker)) - nil) - (org-gtd-projects-fix-todo-keywords heading-marker))) - (org-gtd-process-inbox)) - -(defun org-gtd--poorly-formatted-project-p () - "Return non-nil if the project is composed of only one heading." - (basic-save-buffer) - (eql 1 (length (org-map-entries t)))) - -(defun org-gtd--show-error-and-return-to-editing () - "Tell the user something is wrong with the project." - (display-message-or-buffer - "A 'project' in GTD is a finite set of steps after which a given task is -complete. In Org GTD, this is defined as a top-level org heading with at least -one second-level org headings. When the item you are editing is intended to be -a project, create such a headline structure, like so: - -* Project heading -** First task -** Second task -** Third task - -If you do not need sub-headings, then make a single action instead.") - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--calendar () - "Process GTD inbox item by scheduling it. - -Allow the user apply user-defined tags from -`org-tag-persistent-alist', `org-tag-alist' or file-local tags in -the inbox. Refile to `org-gtd-actionable-file-basename'." - (interactive) - (org-gtd--decorate-item) - (org-schedule 0) - (org-gtd--refile org-gtd-calendar) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--delegate () - "Process GTD inbox item by delegating it. -Allow the user apply user-defined tags from -`org-tag-persistent-alist', `org-tag-alist' or file-local tags in -the inbox. Set it as a waiting action and refile to -`org-gtd-actionable-file-basename'." - (interactive) - (org-gtd--decorate-item) - (org-gtd-delegate) - (org-gtd--refile org-gtd-actions) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--incubate () - "Process GTD inbox item by incubating it. -Allow the user apply user-defined tags from -`org-tag-persistent-alist', `org-tag-alist' or file-local tags in -the inbox. Refile to any org-gtd incubate target (see manual)." - (interactive) - (org-gtd--decorate-item) - (org-schedule 0) - (org-gtd--refile org-gtd-incubated) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--quick-action () - "Process GTD inbox item by doing it now. -Allow the user apply user-defined tags from -`org-tag-persistent-alist', `org-tag-alist' or file-local tags in -the inbox. Mark it as done and archive." - (interactive) - (org-back-to-heading) - (org-gtd--decorate-item) - (org-todo "DONE") - (with-org-gtd-context (org-archive-subtree)) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--single-action () - "Process GTD inbox item as a single action. -Allow the user apply user-defined tags from -`org-tag-persistent-alist', `org-tag-alist' or file-local tags in -the inbox. Set as a NEXT action and refile to -`org-gtd-actionable-file-basename'." - (interactive) - (org-gtd--decorate-item) - (org-todo "NEXT") - (org-gtd--refile org-gtd-actions) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--trash () - "Mark GTD inbox item as cancelled and archive it." - (interactive) - (org-gtd--decorate-item) - (org-todo "CNCL") - (with-org-gtd-context (org-archive-subtree)) - (org-gtd-process-inbox)) - -;;;###autoload -(defun org-gtd--stop-processing () - "Private function. - -Stop processing the inbox." - (interactive) - (widen) - (org-gtd-process-mode -1) - (whitespace-cleanup)) - -(defun org-gtd--decorate-item () - "Apply hooks to add metadata to a given GTD item." - (goto-char (point-min)) - (dolist (hook org-gtd-process-item-hooks) - (save-excursion - (save-restriction - (funcall hook))))) - -(provide 'org-gtd-inbox-processing) -;;; org-gtd-inbox-processing.el ends here diff --git a/org-gtd-incubate.el b/org-gtd-incubate.el new file mode 100644 index 0000000..6295461 --- /dev/null +++ b/org-gtd-incubate.el @@ -0,0 +1,87 @@ +;;; org-gtd-incubate.el --- Define incubated items in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Incubated items have their own logic, defined here +;; +;;; Code: + +(defconst org-gtd-incubate "Incubated") + +(defconst org-gtd-incubate-template + (format "* Incubate +:PROPERTIES: +:ORG_GTD: %s +:END: +" org-gtd-incubate) + "Template for the GTD someday/maybe list.") + +(defcustom org-gtd-organize-incubate-func + #'org-gtd-incubate--apply + "Function called when item at point is to be incubated." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +;;;###autoload +(defun org-gtd-incubate (&optional reminder-date) + "Decorate, organize and refile item at point as incubated. + +If you want to call this non-interactively, +REMINDER-DATE is the YYYY-MM-DD string for when you want this to come up again." + (interactive) + (org-gtd-organize--call + (apply-partially org-gtd-organize-incubate-func + reminder-date))) + +(defun org-gtd-incubate--apply (&optional reminder-date) + "Incubate this item through org-gtd. + +If you want to call this non-interactively, +REMINDER-DATE is the YYYY-MM-DD string for when you want this to come up again." + (let ((date (or reminder-date + (org-read-date t nil nil "When would you like this item to come up again? ")))) + (org-entry-put (point) org-gtd-timestamp (format "<%s>" date)) + (save-excursion + (org-end-of-meta-data t) + (open-line 1) + (insert (format "<%s>" date)))) + (setq-local org-gtd--organize-type 'incubated) + (org-gtd-organize-apply-hooks) + (org-gtd--refile org-gtd-incubate org-gtd-incubate-template)) + +(defun org-gtd-incubate-create (topic reminder-date) + "Automatically create a delegated task in the GTD flow. + +TOPIC is the string you want to see in the `org-agenda' view. +REMINDER-DATE is the YYYY-MM-DD string for when you want this to come up again." + (let ((buffer (generate-new-buffer "Org GTD programmatic temp buffer")) + (org-id-overriding-file-name "org-gtd")) + (with-current-buffer buffer + (org-mode) + (insert (format "* %s" topic)) + (org-gtd-clarify-item) + (org-gtd-incubate reminder-date)) + (kill-buffer buffer))) + + +(provide 'org-gtd-incubate) +;;; org-gtd-incubate.el ends here diff --git a/org-gtd-knowledge.el b/org-gtd-knowledge.el new file mode 100644 index 0000000..bd617a8 --- /dev/null +++ b/org-gtd-knowledge.el @@ -0,0 +1,57 @@ +;;; org-gtd-knowledge.el --- Define logic for handling knowledge in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; GTD needs logic to handle items that are knowledge, this is it. +;; +;;; Code: + +(require 'org-gtd-core) +(require 'org-gtd-archive) + +(defcustom org-gtd-knowledge-func + #'org-gtd-knowledge--apply + "Function called when item at point is knowledge to be stored. +Note that this function is used inside loops,for instance to process the inbox, +so if you have manual steps you need to take when storing a heading +as knowledge, take them before calling this function +\(for instance, during inbox processing, take the manual steps during the +clarify step, before you call `org-gtd-organize')." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +;;;###autoload +(defun org-gtd-knowledge () + "Decorate, organize and refile item at point as knowledge." + (interactive) + (org-gtd-organize--call org-gtd-knowledge-func)) + +(defun org-gtd-knowledge--apply () + "Once the user has filed this knowledge, we can execute this logic." + (org-todo org-gtd-done) + (setq-local org-gtd--organize-type 'knowledge) + (org-gtd-organize-apply-hooks) + (with-org-gtd-context + (org-gtd-archive-item-at-point))) + +(provide 'org-gtd-knowledge) +;;; org-gtd-knowledge.el ends here diff --git a/org-gtd-mode.el b/org-gtd-mode.el index 70a76ee..e92b845 100644 --- a/org-gtd-mode.el +++ b/org-gtd-mode.el @@ -26,6 +26,7 @@ (require 'org-agenda) (require 'org-edna) + (require 'org-gtd-core) (defvar org-gtd-edna-inheritance nil "Private.") diff --git a/org-gtd-oops.el b/org-gtd-oops.el new file mode 100644 index 0000000..e8a1550 --- /dev/null +++ b/org-gtd-oops.el @@ -0,0 +1,75 @@ +;;; org-gtd-oops.el --- Define view for missed events in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Life doesn't go as we expect sometimes. Here we can find all the things +;; that for did not get updated when they should have. +;; +;;; Code: + +(defun org-gtd-oops () + "Agenda view for all non-respected timely events." + (interactive) + (with-org-gtd-context + (let ((org-agenda-custom-commands + '(("o" "Show oopses" + ((tags "+DELEGATED_TO={.+}" + ((org-agenda-overriding-header "Missed check-ins on delegated items") + (org-agenda-skip-additional-timestamps-same-entry t) + (org-agenda-skip-function 'org-gtd-skip-unless-timestamp-in-the-past) + )) + (tags "+ORG_GTD=\"Calendar\"+LEVEL=2" + ((org-agenda-overriding-header "Missed appointments") + (org-agenda-skip-additional-timestamps-same-entry t) + (org-agenda-skip-function + '(org-gtd-skip-AND + '(org-gtd-skip-unless-calendar + org-gtd-skip-unless-timestamp-in-the-past)) + ))) + ;; (tags "+LEVEL=2+ORG_GTD=\"Projects\"+DEADLINE<\"\"" + ;; ((org-agenda-overriding-header "??"))) + ;; (tags "+LEVEL=2+ORG_GTD=\"Projects\"+SCHEDULED<\"\"" + ;; ((org-agenda-overriding-header "!!"))) + (agenda "" + ((org-agenda-overriding-header "Projects that should have finished") + (org-agenda-entry-types '(:deadline)) + (org-agenda-skip-deadline-prewarning-if-scheduled nil) + (org-agenda-include-deadlines t) + (org-deadline-warning-days 0) + (org-agenda-span 1) + (org-agenda-skip-function + 'org-gtd-skip-unless-deadline-in-the-past))) + (agenda "" + ((org-agenda-overriding-header "Projects that should have started") + (org-agenda-entry-types '(:scheduled)) + (org-agenda-skip-scheduled-delay-if-deadline nil) + (org-agenda-skip-scheduled-if-deadline-is-shown nil) + (org-agenda-span 1) + (org-agenda-skip-function + '(org-gtd-skip-AND + '(org-gtd-skip-if-habit + org-gtd-skip-unless-scheduled-start-in-the-past))))) + ))))) + (org-agenda nil "o") + (goto-char (point-min))))) + +(provide 'org-gtd-oops) +;;; org-gtd-oops.el ends here diff --git a/org-gtd-organize.el b/org-gtd-organize.el new file mode 100644 index 0000000..9f02c08 --- /dev/null +++ b/org-gtd-organize.el @@ -0,0 +1,148 @@ +;;; org-gtd-organize.el --- Move tasks where they belong -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Move tasks where they need to be in the org-gtd system. +;; +;;; Code: + +(require 'transient) + +(require 'org-gtd-backward-compatibility) +(require 'org-gtd-core) +(require 'org-gtd-clarify) +(require 'org-gtd-calendar) +(require 'org-gtd-habit) +(require 'org-gtd-knowledge) +(require 'org-gtd-incubate) +(require 'org-gtd-quick-action) +(require 'org-gtd-single-action) +(require 'org-gtd-trash) +(require 'org-gtd-delegate) +(require 'org-gtd-agenda) +(require 'org-gtd-projects) +(require 'org-gtd-refile) + +(defgroup org-gtd-organize nil + "Manage the functions for organizing the GTD actions." + :package-version '(org-gtd . "3.0.0") + :group 'org-gtd) + +(defcustom org-gtd-organize-hooks '(org-set-tags-command) + "Enhancements to add to each item as they get processed from the inbox. + +This is a list of functions that modify an org element. The default value has +one function: setting org tags on the item. Some built-in examples are +provided as options here. You can create your own functions to further organize +the items once they have been processed and add them to that list. + +Once you have your ground items managed, you might like to set the variable +`org-gtd-areas-of-focus' and add `org-gtd-set-area-of-focus' to these hooks." + :group 'org-gtd + :package-version '(org-gtd . "1.0.4") + :type 'hook + :options '(org-set-tags-command org-set-effort org-priority)) + +(defvar-local org-gtd--organize-type nil + "Type of action chosen by the user for this one item.") + +(defconst org-gtd-organize-action-types + '(quick-action single-action calendar habit + delegated incubated knowledge trash + project-heading project-task everything) + "Valid actions types as input for `org-gtd-organize-type-member-p'.") + +(define-error + 'org-gtd-invalid-organize-action-type-error + "At least one element of %s is not in %s" + 'org-gtd-error) + +(transient-define-prefix org-gtd-organize () + "Choose how to categorize the current item." + ["Actionable" + [("q" "Quick action" org-gtd-quick-action) + ("s" "Single action" org-gtd-single-action)] + [("d" "Delegate" org-gtd-delegate) + ("c" "Calendar" org-gtd-calendar) + ("h" "Habit" org-gtd-habit)]] + [("p" "Project (multi-step)" org-gtd-project-new) + ("a" "Add this task to an existing project" org-gtd-project-extend)] + ["Non-actionable" + [("i" "Incubate" org-gtd-incubate) + ("k" "Knowledge to be stored" org-gtd-knowledge)] + [("t" "Trash" org-gtd-trash)]]) + +(defun org-gtd-organize--call (func) + "Wrap FUNC, which does the real work, to keep Emacs clean. +This handles the internal bits of `org-gtd'." + (goto-char (point-min)) + (when (org-before-first-heading-p) + (org-next-visible-heading 1)) + (catch 'org-gtd-error + (with-org-gtd-context + (save-excursion (funcall func))) + (let ((loop-p (and (boundp org-gtd-clarify--inbox-p) org-gtd-clarify--inbox-p)) + (task-id org-gtd-clarify--clarify-id) + (window-config org-gtd-clarify--window-config) + (buffer (marker-buffer org-gtd-clarify--source-heading-marker)) + (position (marker-position org-gtd-clarify--source-heading-marker))) + (with-current-buffer buffer + (goto-char position) + (org-cut-subtree)) + (set-window-configuration window-config) + (kill-buffer (org-gtd-clarify--buffer-name task-id)) + (if loop-p (org-gtd-process-inbox))))) + +(defun org-gtd-organize-apply-hooks () + "Apply hooks to add metadata to a given GTD item." + (dolist (hook org-gtd-organize-hooks) + (save-excursion + (goto-char (point-min)) + (when (org-before-first-heading-p) + (org-next-visible-heading 1)) + (save-restriction (funcall hook))))) + +(defun org-gtd-organize-type-member-p (list) + "Return t if the action type chosen by the user is in LIST. + +Valid members of LIST include: +- 'quick-action (done in less than two minutes) +- 'single-action (do when possible) +- 'calendar (do at a given time) +- 'delegated (done by someone else) +- 'habit (a recurring action) +- 'incubated (remind me later) +- 'knowledge (stored as reference) +- 'trash (self-explanatory) +- 'project-heading (top-level project info, e.g. area of focus) +- 'project-task (task-specific info, similar in spirit to single-action) +- 'everything (if this is in the list, always return t)" + (let ((list (ensure-list list))) + (unless (seq-every-p + (lambda (x) (member x org-gtd-organize-action-types)) + list) + (signal 'org-gtd-invalid-organize-action-type-error + `(,list ,org-gtd-organize-action-types))) + (or (member 'everything list) + (member org-gtd--organize-type list)))) + +(provide 'org-gtd-organize) +;;; org-gtd-organize.el ends here diff --git a/org-gtd-pkg.el b/org-gtd-pkg.el index 5e5fbeb..5c40bf4 100644 --- a/org-gtd-pkg.el +++ b/org-gtd-pkg.el @@ -1,6 +1,6 @@ -(define-package "org-gtd" "2.3.0" +(define-package "org-gtd" "3.0.0" "An implementation of GTD." - '((emacs "27.1") + '((emacs "27.2") (org-edna "1.1.2") (f "0.20.0") (org "9.6") diff --git a/org-gtd-process.el b/org-gtd-process.el new file mode 100644 index 0000000..d33f39f --- /dev/null +++ b/org-gtd-process.el @@ -0,0 +1,55 @@ +;;; org-gtd-process.el --- Code to process inbox -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Inbox processing management for org-gtd. +;; +;;; Code: + +(require 'org-gtd-core) +(require 'org-gtd-agenda) +(require 'org-gtd-projects) +(require 'org-gtd-refile) +(require 'org-gtd-organize) + +;;;###autoload +(defun org-gtd-process-inbox () + "Start the inbox processing item, one heading at a time." + (interactive) + + (let ((buffer (find-file-noselect (org-gtd-inbox-path)))) + (set-buffer buffer) + (condition-case _err + (progn + (goto-char (point-min)) + (when (org-before-first-heading-p) + (outline-next-visible-heading 1)) + (org-N-empty-lines-before-current 1) + (org-gtd-clarify-inbox-item)) + (user-error (org-gtd-process--stop))))) + +(defun org-gtd-process--stop () + "Stop processing the inbox." + (interactive) + (whitespace-cleanup)) + +(provide 'org-gtd-process) +;;; org-gtd-process.el ends here diff --git a/org-gtd-projects.el b/org-gtd-projects.el index 2901600..064ca19 100644 --- a/org-gtd-projects.el +++ b/org-gtd-projects.el @@ -31,8 +31,62 @@ (require 'org-gtd-core) (require 'org-gtd-agenda) +(defcustom org-gtd-organize-project-func + #'org-gtd-project-new--apply + "Function called when item at point is a project. + +You *probably* should not change this from the default, as a lot of fiddly bits +depend on the way org-gtd structures and organizes the projects." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +(defcustom org-gtd-organize-add-to-project-func + #'org-gtd-project-extend--apply + "Function called when item at point is a new task in an existing project." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +(defconst org-gtd-project-headings + "+LEVEL=2&+ORG_GTD=\"Projects\"" + "How to tell `org-mode' to find project headings.") + +(defconst org-gtd-stuck-projects + `(,org-gtd-project-headings + (,org-gtd-next ,org-gtd-wait) + nil + "") + "How to identify stuck projects in the GTD system. + +This is a list of four items, the same type as in `org-stuck-projects'.") + +(defconst org-gtd-projects "Projects") + +(defconst org-gtd-projects-template + (format "* Projects +:PROPERTIES: +:TRIGGER: org-gtd-next-project-action org-gtd-update-project-task! +:ORG_GTD: %s +:END: +" org-gtd-projects)) + +(defconst org-gtd-projects--malformed + "A 'project' in GTD is a finite set of steps after which a given task is +complete. In Org GTD, this is defined as a top-level org heading with at least +one second-level org headings. When the item you are editing is intended to be +a project, create such a headline structure, like so: + +* Project heading +** First task +** Second task +** Third task + +If you do not need sub-headings, then organize this item as a 'single action' +instead.") + ;;;###autoload -(defun org-gtd-cancel-project () +(defun org-gtd-project-cancel () "With point on topmost project heading, mark all undone tasks canceled." (interactive) (org-edna-mode -1) @@ -41,71 +95,203 @@ (lambda () (when (org-gtd-projects--incomplete-task-p) (let ((org-inhibit-logging 'note)) - (org-todo "CNCL")))) + (org-todo org-gtd-canceled)))) nil 'tree)) (org-edna-mode 1)) ;;;###autoload -(defun org-gtd-show-stuck-projects () - "Show all projects that do not have a next action." +(defun org-gtd-project-cancel-from-agenda () + "Cancel the project that has the highlighted task." + (declare (modes org-agenda-mode)) ;; for 27.2 compatibility + (interactive "i") + (org-agenda-check-type t 'agenda 'todo 'tags 'search) + (org-agenda-check-no-diary) + (let* ((marker (or (org-get-at-bol 'org-marker) + (org-agenda-error))) + (buffer (marker-buffer marker)) + (pos (marker-position marker))) + (set-marker-insertion-type marker t) + (org-with-remote-undo buffer + (with-current-buffer buffer + (widen) + (goto-char pos) + (org-up-heading-safe) + (org-gtd-project-cancel))))) + +;;;###autoload +(defun org-gtd-project-new () + "Organize, decorate and refile item as a new project." + (interactive) + (org-gtd-organize--call org-gtd-organize-project-func)) + +;;;###autoload +(defun org-gtd-project-extend () + "Organize, decorate and refile item as a new task in an existing project." (interactive) + (org-gtd-organize--call org-gtd-organize-add-to-project-func)) + +(defun org-gtd-project-new--apply () + "Process GTD inbox item by transforming it into a project. + +Allow the user apply user-defined tags from `org-tag-persistent-alist', +`org-tag-alist' or file-local tags in the inbox. +Refile to `org-gtd-actionable-file-basename'." + (when (org-gtd-projects--poorly-formatted-p) + (org-gtd-projects--show-error) + (throw 'org-gtd-error "Malformed project")) + + (setq-local org-gtd--organize-type 'project-heading) + (org-gtd-organize-apply-hooks) + + (setq-local org-gtd--organize-type 'project-task) + (org-gtd-projects--apply-organize-hooks-to-tasks) + + (org-gtd-projects-fix-todo-keywords-for-project-at-point) + + (let ((org-special-ctrl-a t)) + (org-end-of-line)) + (insert " [/]") + (org-update-statistics-cookies t) + (org-gtd--refile org-gtd-projects org-gtd-projects-template)) + +(defun org-gtd-project-extend--apply () + "Refile the org heading at point under a chosen heading in the agenda files." (with-org-gtd-context - (org-agenda-list-stuck-projects))) + (let* ((org-gtd-refile-to-any-target nil) + (org-use-property-inheritance '("ORG_GTD")) + (headings (org-map-entries + (lambda () (org-get-heading t t t t)) + org-gtd-project-headings + 'agenda)) + (chosen-heading (completing-read "Choose a heading: " headings nil t)) + (heading-marker (org-find-exact-heading-in-directory chosen-heading org-gtd-directory))) + (setq-local org-gtd--organize-type 'project-task) + (org-gtd-organize-apply-hooks) + (org-refile 3 nil `(,chosen-heading + ,(buffer-file-name (marker-buffer heading-marker)) + nil + ,(marker-position heading-marker)) + nil) + (org-gtd-projects-fix-todo-keywords heading-marker)))) + +(defun org-gtd-projects--apply-organize-hooks-to-tasks () + "Decorate tasks for project at point." + (org-map-entries + (lambda () + (org-narrow-to-element) + (org-gtd-organize-apply-hooks) + (widen)) + "LEVEL=2" + 'tree)) ;;;###autoload (defun org-gtd-projects-fix-todo-keywords-for-project-at-point () "Ensure keywords for subheadings of project at point are sane. -This means one and only one NEXT keyword, and it is the first of type TODO -in the list." +This means one and only one `org-gtd-next' keyword, and it is the first non-done +state in the list - all others are `org-gtd-todo'." (interactive) (org-gtd-projects-fix-todo-keywords (point-marker))) -(defun org-gtd-projects-fix-todo-keywords (marker) - "Ensure project at MARKER has only one NEXT keyword. Ensures only the first non-done keyword is NEXT, all other non-done are TODO." - (with-current-buffer (marker-buffer marker) - (save-excursion - (goto-char (marker-position marker)) - ;; first, make sure all we have is TODO WAIT DONE CNCL - (org-map-entries - (lambda () - (unless (member - (org-element-property :todo-keyword (org-element-at-point)) - '("TODO" "WAIT" "DONE" "CNCL")) - (org-entry-put (org-gtd-projects--org-element-pom (org-element-at-point)) "TODO" "TODO"))) - "+LEVEL=3" 'tree)) +(defun org-gtd-projects-fix-todo-keywords (heading-marker) + "Ensure the project tasks under heading at HEADING-MARKER have at most +one `org-gtd-next' or `org-gtd-wait' task and all other undone tasks +are marked as `org-gtd-todo'." + (let* ((buffer (marker-buffer heading-marker)) + (position (marker-position heading-marker)) + (heading-level (with-current-buffer buffer + (goto-char position) + (org-current-level)))) (save-excursion - (goto-char (marker-position marker)) - (let* ((tasks (org-map-entries #'org-element-at-point "+LEVEL=3" 'tree)) - (first-wait (-any (lambda (x) (and (string-equal "WAIT" (org-element-property :todo-keyword x)) x)) tasks)) - (first-todo (-any (lambda (x) (and (string-equal "TODO" (org-element-property :todo-keyword x)) x)) tasks))) - (unless first-wait - (org-entry-put (org-gtd-projects--org-element-pom first-todo) "TODO" "NEXT")))))) + (with-current-buffer buffer + (org-gtd-core-prepare-buffer) + (goto-char position) -(defun org-gtd-projects--org-element-pom (element) - "Return buffer position for start of Org ELEMENT." - (org-element-property :begin element)) + (org-map-entries + (lambda () + (unless (or (equal heading-level (org-current-level)) + (member (org-entry-get (point) "TODO") + `(,org-gtd-todo ,org-gtd-wait ,org-gtd-done ,org-gtd-canceled))) + (org-entry-put (point) "TODO" org-gtd-todo) + (setq org-map-continue-from (end-of-line)))) + t + 'tree) + (let ((first-wait (org-gtd-projects--first-wait-task)) + (first-todo (org-gtd-projects--first-todo-task))) + (unless first-wait + (org-entry-put (org-gtd-projects--org-element-pom first-todo) + "TODO" + org-gtd-next))))))) -;; TODO rename to something like initialize TODO states -(defun org-gtd-projects--nextify () - "Add the NEXT keyword to the first action/task of the project. - -Add the TODO keyword to all subsequent actions/tasks." - (cl-destructuring-bind - (first-entry . rest-entries) - (cdr (org-map-entries (lambda () (org-element-at-point)) t 'tree)) - (org-element-map - (reverse rest-entries) - 'headline - (lambda (myelt) - (org-entry-put (org-gtd-projects--org-element-pom myelt) "TODO" "TODO"))) - (org-entry-put (org-gtd-projects--org-element-pom first-entry) "TODO" "NEXT"))) +(defun org-gtd-projects--first-wait-task () + "Given an org tree at point, return the first subtask with `org-gtd-wait'. +Return `nil' if there isn't one." + (let ((heading-level (org-current-level))) + (car + (seq-filter (lambda (x) x) + (org-map-entries + (lambda () + (and (not (equal heading-level (org-current-level))) + (string-equal org-gtd-wait + (org-entry-get (point) "TODO")) + (org-element-at-point))) + t + 'tree))))) + +(defun org-gtd-projects--first-todo-task () + "Given an org tree at point, return the first subtask with `org-gtd-todo'. +Return `nil' if there isn't one." + (let ((heading-level (org-current-level))) + (car + (seq-filter (lambda (x) x) + (org-map-entries + (lambda () + (and (not (equal heading-level (org-current-level))) + (string-equal org-gtd-todo + (org-entry-get (point) "TODO")) + (org-element-at-point))) + t + 'tree))))) (defun org-gtd-projects--incomplete-task-p () "Determine if current heading is a task that's not finished." (and (org-entry-is-todo-p) (not (org-entry-is-done-p)))) +(defun org-gtd-projects--org-element-pom (element) + "Return buffer position for start of Org ELEMENT." + (org-element-property :begin element)) + +(defun org-gtd-projects--poorly-formatted-p () + "Return non-nil if the project is composed of only one heading." + (eql 1 (length (org-map-entries t)))) + +(defun org-gtd-projects--show-error () + "Tell the user something is wrong with the project." + (let ((resize-mini-windows t) + (max-mini-window-height 0)) + (display-message-or-buffer org-gtd-projects--malformed)) + ;; read-key changed in emacs 28 + (if (version< emacs-version "28") + (read-key "Waiting for a keypress to return to clarifying... ") + (read-key "Waiting for a keypress to return to clarifying... " t)) + + (message "")) + +(defun org-gtd-projects--edna-update-project-task (_last-entry) + "`org-edna' extension to change the todo state to `org-gtd-next'." + (org-todo org-gtd-next)) + +(defalias 'org-edna-action/org-gtd-update-project-task! + 'org-gtd-projects--edna-update-project-task) + +(defun org-gtd-projects--edna-next-project-action () + "`org-edna' extension to find the next action to show in the agenda." + (org-edna-finder/relatives 'forward-no-wrap 'todo-only 1 'no-sort)) + +(defalias 'org-edna-finder/org-gtd-next-project-action + 'org-gtd-projects--edna-next-project-action) + (provide 'org-gtd-projects) ;;; org-gtd-projects.el ends here diff --git a/org-gtd-quick-action.el b/org-gtd-quick-action.el new file mode 100644 index 0000000..59f62d1 --- /dev/null +++ b/org-gtd-quick-action.el @@ -0,0 +1,48 @@ +;;; org-gtd-quick-action.el --- Define quick-action items in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Quick action items have their own logic, defined here +;; +;;; Code: + +(defcustom org-gtd-organize-quick-action-func + #'org-gtd-quick-action--apply + "Function called when item at point was quick action." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +(defun org-gtd-quick-action--apply () + "Process GTD inbox item by doing it now." + (org-todo org-gtd-done) + (setq-local org-gtd--organize-type 'quick-action) + (org-gtd-organize-apply-hooks) + (with-org-gtd-context (org-archive-subtree))) + +;;;###autoload +(defun org-gtd-quick-action () + "Organize, decorate and refile item at point as a quick action." + (interactive) + (org-gtd-organize--call org-gtd-organize-quick-action-func)) + +(provide 'org-gtd-quick-action) +;;; org-gtd-quick-action.el ends here diff --git a/org-gtd-refile.el b/org-gtd-refile.el index 053fd90..a96e21e 100644 --- a/org-gtd-refile.el +++ b/org-gtd-refile.el @@ -27,54 +27,23 @@ (require 'org) (require 'org-refile) (require 'org-element) + (require 'org-gtd-core) -(defconst org-gtd-projects-template - "* Projects -:PROPERTIES: -:TRIGGER: relatives(forward-no-wrap todo-only 1 no-sort) todo!(NEXT) -:ORG_GTD: Projects -:END: -") - -(defconst org-gtd-calendar-template - "* Calendar -:PROPERTIES: -:ORG_GTD: Calendar -:END: -") - -(defconst org-gtd-actions-template - "* Actions -:PROPERTIES: -:ORG_GTD: Actions -:END: -") - -(defconst org-gtd-incubated-template - "* Incubate -:PROPERTIES: -:ORG_GTD: Incubated -:END: -" - "Template for the GTD someday/maybe list.") - -(defconst org-gtd--file-template - (let ((myhash (make-hash-table :test 'equal))) - (puthash org-gtd-actions org-gtd-actions-template myhash) - (puthash org-gtd-calendar org-gtd-calendar-template myhash) - (puthash org-gtd-projects org-gtd-projects-template myhash) - (puthash org-gtd-incubated org-gtd-incubated-template myhash) - myhash)) - -(defconst org-gtd-refile--prompt - (let ((myhash (make-hash-table :test 'equal))) - (puthash org-gtd-actions "Refile single action to: " myhash) - (puthash org-gtd-incubated "Refile incubated item to: " myhash) - (puthash org-gtd-delegated "Refile delegated item to: " myhash) - (puthash org-gtd-projects "Refile project to: " myhash) - (puthash org-gtd-calendar "Refile calendar item to: " myhash) - myhash)) +(defcustom org-gtd-refile-to-any-target t + "Set to true if you do not need to choose where to refile processed items. + +When this is true, org-gtd will refile to the first target it finds, or creates +it if necessary, without confirmation. When this is false, it will ask for +confirmation regardless of the number of options. Note that setting this to +false does not mean you can safely create new targets. See the documentation +to create new refile targets. + +Defaults to true to carry over pre-2.0 behavior. You will need to change this +setting as part of following the instructions to add your own refile targets." + :group 'org-gtd + :type 'boolean + :package-version '(org-gtd . "2.0.0")) ;;;###autoload (defmacro with-org-gtd-refile (type &rest body) @@ -87,38 +56,31 @@ TYPE is the org-gtd action type. BODY is the rest of the code." (unwind-protect (with-org-gtd-context (progn ,@body))))) -(defun org-gtd--refile (type) +(defun org-gtd--refile (type refile-target-element) "Refile an item to the single action file. -TYPE is one of the org-gtd action types. This is a private function." +TYPE is one of the org-gtd action types." (with-org-gtd-refile type - (unless (org-refile-get-targets) (org-gtd-refile--add-target type)) + (unless (org-refile-get-targets) (org-gtd-refile--add-target refile-target-element)) (if org-gtd-refile-to-any-target (org-refile nil nil (car (org-refile-get-targets))) - (org-refile nil nil nil (org-gtd-refile--prompt type))))) + (org-refile nil nil nil "Finish organizing task under: ")))) -(defun org-gtd-refile--add-target (gtd-type) +(defun org-gtd-refile--add-target (refile-target-element) "Private function used to create a missing org-gtd refile target. GTD-TYPE is an action type." + (print "inserting the new refile target") (with-current-buffer (org-gtd--default-file) (goto-char (point-max)) (newline) - (insert (gethash gtd-type org-gtd--file-template)) + (insert refile-target-element) (basic-save-buffer))) (defun org-gtd-refile--group-p (type) "Determine whether the current heading is of a given gtd TYPE." - (string-equal (org-gtd-refile--group type) + (string-equal type (org-element-property :ORG_GTD (org-element-at-point)))) -(defun org-gtd-refile--group (type) - "What kind of gtd group is TYPE." - (gethash type org-gtd--properties)) - -(defun org-gtd-refile--prompt (type) - "What is the right refile prompt for this gtd TYPE." - (gethash type org-gtd-refile--prompt)) - (provide 'org-gtd-refile) ;;; org-gtd-refile.el ends here diff --git a/org-gtd-review.el b/org-gtd-review.el new file mode 100644 index 0000000..777e571 --- /dev/null +++ b/org-gtd-review.el @@ -0,0 +1,174 @@ +;;; org-gtd-review.el --- GTD review logic for org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Reviews are a crucial part of GTD. This code determines how to use +;; the agenda views for review purposes. +;; +;;; Code: + +(require 'org) +(require 'org-gtd-core) +(require 'org-gtd-areas-of-focus) +(require 'org-gtd-skip) +(require 'org-gtd-agenda) + +(define-error + 'org-gtd-invalid-area-of-focus + "`%s' is not a member of `%s'" + 'org-gtd-error) + +;;;###autoload +(defun org-gtd-review-area-of-focus (&optional area start-date) + "Generate an overview agenda for a given area of focus. + +You can pass an optional AREA (must be a member of `org-gtd-areas-of-focus') to +skip the menu to choose one. +START-DATE tells the code what to use as the first day for the agenda. It is +mostly of value for testing purposes." + (interactive (list (completing-read + "Which area of focus would you like to review? " + org-gtd-areas-of-focus + nil + t))) + (when (not (member area org-gtd-areas-of-focus)) + (signal 'org-gtd-invalid-area-of-focus `(,area ,org-gtd-areas-of-focus))) + + (let ((start-date (or start-date (format-time-string "%Y-%m-%d")))) + (org-gtd-core-prepare-agenda-buffers) + (with-org-gtd-context + (let ((org-agenda-custom-commands + `(("a" ,(format "Area of Focus: %s" area) + ((tags ,org-gtd-project-headings + ((org-agenda-overriding-header "Active projects"))) + + (todo ,org-gtd-next + ((org-agenda-overriding-header "Next actions"))) + + (agenda "" + ((org-agenda-overriding-header "Reminders") + (org-agenda-start-day ,start-date) + (org-agenda-show-all-dates nil) + (org-agenda-show-future-repeats nil) + (org-agenda-span 90) + (org-agenda-include-diary nil) + (org-agenda-skip-additional-timestamps-same-entry t) + (org-agenda-skip-function + '(org-gtd-skip-AND '(org-gtd-skip-unless-calendar + ,(org-gtd-skip-unless-area-of-focus-func area)))))) + + (agenda "" + ((org-agenda-overriding-header "Routines") + (org-agenda-time-grid '((require-timed) () "" "")) + (org-agenda-entry-types '(:scheduled)) + (org-agenda-start-day ,start-date) + (org-agenda-span 'day) + (org-habit-show-habits-only-for-today nil) + (org-agenda-skip-function + '(org-gtd-skip-AND '(org-gtd-skip-unless-habit + ,(org-gtd-skip-unless-area-of-focus-func area)))))) + (tags ,(format "+ORG_GTD=\"%s\"+%s>\"<%s>\"" + org-gtd-incubate + org-gtd-timestamp + start-date) + ((org-agenda-overriding-header "Incubated items")))) + ((org-agenda-skip-function '(org-gtd-skip-unless-area-of-focus ,area)) + (org-agenda-buffer-name ,(format "*Org Agenda: %s*" area))))))) + (org-agenda nil "a") + (goto-char (point-min)))))) + +(defun org-gtd-review-stuck-calendar-items () + "Agenda view with all invalid Calendar actions." + (interactive) + (with-org-gtd-context + (let ((org-agenda-custom-commands + '(("g" "foobar" + ((tags "+ORG_GTD=\"Calendar\"+LEVEL=2" + ((org-agenda-include-diary nil) + (org-agenda-skip-function + 'org-gtd-skip-unless-timestamp-empty-or-invalid) + (org-agenda-skip-additional-timestamps-same-entry t) + ))))))) + (org-agenda nil "g")))) + +(defun org-gtd-review-stuck-incubated-items () + "Agenda view with all invalid Calendar actions." + (interactive) + (with-org-gtd-context + (let ((org-agenda-custom-commands + '(("g" "foobar" + ((tags "ORG_GTD=\"Incubated\"" + ((org-agenda-skip-function + 'org-gtd-skip-unless-timestamp-empty-or-invalid) + (org-agenda-skip-additional-timestamps-same-entry t) + ))))))) + (org-agenda nil "g")))) + +(defun org-gtd-review-stuck-habit-items () + "Agenda view with all invalid Calendar actions." + (interactive) + (with-org-gtd-context + (let ((org-agenda-custom-commands + '(("g" "foobar" + ((tags "ORG_GTD=\"Habits\"" + ((org-agenda-skip-function + 'org-gtd-skip-unless-timestamp-empty-or-invalid) + (org-agenda-skip-additional-timestamps-same-entry t) + ))))))) + (org-agenda nil "g")))) + +(defun org-gtd-review-stuck-delegated-items () + "Agenda view with all invalid Calendar actions." + (interactive) + (with-org-gtd-context + (let ((org-agenda-custom-commands + `(("g" "foobar" + ((tags (format "+TODO=\"%s\"" org-gtd-wait) + ((org-agenda-skip-function + '(org-gtd-skip-AND + '(org-gtd-skip-unless-timestamp-empty-or-invalid + org-gtd-skip-unless-delegated-to-empty))) + (org-agenda-skip-additional-timestamps-same-entry t) + ))))))) + (org-agenda nil "g")))) + +(defun org-gtd-review-stuck-single-action-items () + "Agenda view with all invalid Calendar actions." + (interactive) + (with-org-gtd-context + (let ((org-agenda-custom-commands + `(("g" "foobar" + ((tags (format "+ORG_GTD=\"%s\"" org-gtd-action) + ((org-agenda-skip-function + 'org-gtd-skip-unless-timestamp-empty-or-invalid) + (org-agenda-skip-additional-timestamps-same-entry t) + ))))))) + (org-agenda nil "g")))) + +;;;###autoload +(defun org-gtd-review-stuck-projects () + "Show all projects that do not have a next action." + (interactive) + (with-org-gtd-context + (org-agenda-list-stuck-projects))) + +(provide 'org-gtd-review) +;;; org-gtd-review.el ends here diff --git a/org-gtd-single-action.el b/org-gtd-single-action.el new file mode 100644 index 0000000..a8c067f --- /dev/null +++ b/org-gtd-single-action.el @@ -0,0 +1,72 @@ +;;; org-gtd-single-action.el --- Define single action items in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Single action items have their own logic, defined here +;; +;;; Code: + +(defconst org-gtd-action "Actions") + +(defconst org-gtd-action-template + (format "* Actions +:PROPERTIES: +:ORG_GTD: %s +:END: +" org-gtd-action)) + +(defcustom org-gtd-organize-single-action-func + #'org-gtd-single-action--apply + "Function called when item at point is a single next action." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +;;;###autoload +(defun org-gtd-single-action () + "Organize, decorate and refile item at point as a single action." + (interactive) + (org-gtd-organize--call org-gtd-organize-single-action-func)) + +(defun org-gtd-single-action--apply () + "Item at point is a one-off action, ready to be executed." + (interactive) + (org-todo org-gtd-next) + (setq-local org-gtd--organize-type 'single-action) + (org-gtd-organize-apply-hooks) + (org-gtd--refile org-gtd-action org-gtd-action-template)) + +(defun org-gtd-single-action-create (topic) + "Automatically create a delegated task in the GTD flow. + +TOPIC is what you want to see in the agenda view." + (let ((buffer (generate-new-buffer "Org GTD programmatic temp buffer")) + (org-id-overriding-file-name "org-gtd")) + (with-current-buffer buffer + (org-mode) + (insert (format "* %s" topic)) + (org-gtd-clarify-item) + (org-gtd-single-action)) + (kill-buffer buffer))) + + +(provide 'org-gtd-single-action) +;;; org-gtd-single-action.el ends here diff --git a/org-gtd-skip.el b/org-gtd-skip.el new file mode 100644 index 0000000..890bb23 --- /dev/null +++ b/org-gtd-skip.el @@ -0,0 +1,160 @@ +;;; org-gtd-skip.el --- various org-agenda-skip-functions -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Building agenda views is complex, and filtering them effectively can truly +;; require its own language. This is that language. +;; +;;; Code: + +(defun org-gtd-skip-AND (funcs) + "Ensure all of the functions FUNCS want to skip the current entry." + (let ((non-nil-funcs (seq-drop-while (lambda (x) (not (funcall x))) funcs))) + (if non-nil-funcs + (funcall (car non-nil-funcs))))) + +(defun org-gtd-skip-unless-calendar () + "Skip-function: only keep this if it's an org-gtd calendar entry." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (and (string-equal (org-entry-get (point) "ORG_GTD" t) + org-gtd-calendar) + (org-entry-get (point) "ORG_GTD_TIMESTAMP")) + nil + subtree-end))) + +(defun org-gtd-skip-unless-project-heading () + "Skip-function: only keep this if it's an org-gtd project heading entry." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (and (equal 2 (org-element-property :level (org-element-at-point))) + (string-equal (org-entry-get (point) "ORG_GTD" t) + org-gtd-projects)) + nil + subtree-end))) + +(defun org-gtd-skip-unless-delegated () + "Skip entry unless it is delegated." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (org-entry-get (point) "DELEGATED_TO") + nil + subtree-end))) + +(defun org-gtd-skip-unless-scheduled-start-in-the-past () + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (scheduled-start (org-entry-get (point) "SCHEDULED")) + (start-of-day (org-gtd-skip--start-of-day (current-time)))) + (if (and scheduled-start + (time-less-p (org-time-string-to-time scheduled-start) + start-of-day)) + nil + subtree-end))) + +(defun org-gtd-skip-unless-deadline-in-the-past () + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (deadline (org-entry-get (point) "DEADLINE")) + (start-of-day (org-gtd-skip--start-of-day (current-time)))) + (if (and deadline + (time-less-p (org-time-string-to-time deadline) + start-of-day)) + nil + subtree-end))) + +(defun org-gtd-skip-unless-timestamp-in-the-past () + "Skip unless ORG_GTD_TIMESTAMP is in the past." + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (timestamp (org-entry-get (point) "ORG_GTD_TIMESTAMP")) + (start-of-day (org-gtd-skip--start-of-day (current-time)))) + (if (and timestamp + (time-less-p (org-time-string-to-time timestamp) + start-of-day)) + nil + subtree-end))) + +(defun org-gtd-skip-unless-habit () + "Skip-function: only keep this if it's a habit." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (string-equal "habit" (org-entry-get (point) "STYLE")) + nil + subtree-end))) + +(defun org-gtd-skip-unless-delegated-to-empty () + "Skip-function: only keep this if it's a habit." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (org-entry-get (point) org-gtd-delegate-property) + nil + subtree-end))) + +(defun org-gtd-skip-unless-timestamp-empty-or-invalid () + "Return non-nil if the current headline's ORG_GTD_TIMESTAMP property is not set, null, or not a date." + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (prop (org-entry-get nil org-gtd-timestamp))) + (if (and prop + (org-string-match-p org-ts-regexp-both prop)) + subtree-end + nil))) + +(defun org-gtd-skip-unless-habit-invalid () + "Return non-nil if the current headline's ORG_GTD_TIMESTAMP property is not set, null, or not a date." + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (style (or (org-entry-get nil "STYLE") "")) + (timestamp (or (org-entry-get nil "SCHEDULED") ""))) + (if (and (string-equal style "habit") + (org-string-match-p org-repeat-re timestamp)) + subtree-end + nil))) + +(defun org-gtd-skip-unless-action-invalid () + "Return non-nil if the action wouldn't show up in the agenda." + (let ((subtree-end (save-excursion (org-end-of-subtree t))) + (invalidp (or (not (org-entry-is-todo-p)) + (org-entry-get nil "TODO" org-gtd-todo))) + ) + (if invalidp + nil + subtree-end))) + +(defun org-gtd-skip-unless-area-of-focus-func (area) + "Return a skip-function to only keep if it's a specific GTD AREA of focus." + (apply-partially #'org-gtd-skip-unless-area-of-focus area)) + +(defun org-gtd-skip-unless-area-of-focus (area) + "Skip-function: only keep this if it's a specific GTD AREA of focus." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (string-equal (downcase area) + (downcase (org-entry-get (point) "CATEGORY"))) + nil + subtree-end))) + +(defun org-gtd-skip-if-habit () + "Skip-function: only keep this if it's a habit." + (let ((subtree-end (save-excursion (org-end-of-subtree t)))) + (if (string-equal "habit" (org-entry-get (point) "STYLE")) + subtree-end + nil))) + +(defun org-gtd-skip--start-of-day (timestamp) + (let ((decoded (decode-time timestamp))) + (setf (nth 0 decoded) 0) + (setf (nth 1 decoded) 0) + (setf (nth 2 decoded) 0) + (apply #'encode-time decoded))) + +(provide 'org-gtd-skip) +;;; org-gtd-skip.el ends here diff --git a/org-gtd-trash.el b/org-gtd-trash.el new file mode 100644 index 0000000..e05667f --- /dev/null +++ b/org-gtd-trash.el @@ -0,0 +1,49 @@ +;;; org-gtd-trash.el --- Define trash items in org-gtd -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Trash items have their own logic, defined here. +;; +;;; Code: + +(defcustom org-gtd-organize-trash-func + #'org-gtd-trash--apply + "Function called when item at point is to be discarded." + :group 'org-gtd-organize + :type 'function + :package-version '(org-gtd . "3.0.0")) + +;;;###autoload +(defun org-gtd-trash () + "Organize and refile item at point as trash." + (interactive) + (org-gtd-organize--call org-gtd-organize-trash-func)) + +(defun org-gtd-trash--apply () + "Mark GTD inbox item as cancelled and move it to the org-gtd task archives." + (org-todo org-gtd-canceled) + (setq-local org-gtd--organize-type 'trash) + (org-gtd-organize-apply-hooks) + (with-org-gtd-context + (org-gtd-archive-item-at-point))) + +(provide 'org-gtd-trash) +;;; org-gtd-trash.el ends here diff --git a/org-gtd-upgrades.el b/org-gtd-upgrades.el new file mode 100644 index 0000000..b11fe3b --- /dev/null +++ b/org-gtd-upgrades.el @@ -0,0 +1,122 @@ +;;; org-gtd-upgrades.el --- Define upgrade logic across org-gtd versions -*- lexical-binding: t; coding: utf-8 -*- +;; +;; Copyright © 2019-2023 Aldric Giacomoni + +;; Author: Aldric Giacomoni +;; This file is not part of GNU Emacs. + +;; This file 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, or (at your option) +;; any later version. + +;; This file 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 file. If not, see . + +;;; Commentary: +;; +;; Major versions aren't backward compatible. This code helps users move +;; their data forward. +;; +;;; Code: + +(require 'org-habit) + +(require 'org-gtd-habit) + +(defun org-gtd-upgrade-v2-to-v3 () + "Use only when upgrading org-gtd from v2 to v3. + +Changes state of org-gtd tasks to move away from incorrectly used SCHEDULED +planning keyword in `org-mode'." + (interactive) + (org-gtd-upgrades-calendar-items-to-v3) + (org-gtd-upgrades-delegated-items-to-v3) + (org-gtd-upgrades-incubated-items-to-v3) + (org-gtd-upgrades-habits-to-v3)) + +(defun org-gtd-upgrades-calendar-items-to-v3 () + "Change calendar items away from SCHEDULED to using a custom property." + (with-org-gtd-context + (org-map-entries + (lambda () + (when (org-gtd-upgrades--scheduled-item-p) + (let ((date (org-entry-get (point) "SCHEDULED"))) + (org-schedule '(4)) ;; pretend I am a universal argument + (org-entry-put (point) org-gtd-timestamp date) + (org-end-of-meta-data t) + (open-line 1) + (insert date)))) + "+ORG_GTD=\"Calendar\"+LEVEL=2" + 'agenda))) + +(defun org-gtd-upgrades-incubated-items-to-v3 () + "Change incubated items away from SCHEDULED to using a custom property." + (with-org-gtd-context + (org-map-entries + (lambda () + (when (org-gtd-upgrades--scheduled-item-p) + (let ((date (org-entry-get (point) "SCHEDULED"))) + (org-schedule '(4)) ;; pretend I am a universal argument + (org-entry-put (point) org-gtd-timestamp date) + (org-end-of-meta-data t) + (open-line 1) + (insert date)))) + "+ORG_GTD=\"Incubated\"+LEVEL=2" + 'agenda))) + +(defun org-gtd-upgrades-delegated-items-to-v3 () + "Change delegated items away from SCHEDULED to using a custom property." + (with-org-gtd-context + (org-map-entries + (lambda () + (when (org-gtd-upgrades--delegated-item-p) + (let ((date (org-entry-get (point) "SCHEDULED"))) + (org-schedule '(4)) ;; pretend I am a universal argument + (org-entry-put (point) org-gtd-timestamp date) + (org-end-of-meta-data t) + (open-line 1) + (insert date)))) + "+ORG_GTD=\"Actions\"+LEVEL=2" + 'agenda))) + +(defun org-gtd-upgrades-habits-to-v3 () + "Move habits from wherever they may be to their own subtree." + (with-org-gtd-context + (org-gtd-refile--add-target org-gtd-habit-template) + + (let ((org-gtd-refile-to-any-target t)) + (org-map-entries #'org-gtd-upgrades--organize-habits-v3 + "+LEVEL=2&+ORG_GTD=\"Actions\"" + 'agenda) + (org-map-entries #'org-gtd-upgrades--organize-habits-v3 + "+LEVEL=2&+ORG_GTD=\"Incubated\"" + 'agenda) + (org-map-entries #'org-gtd-upgrades--organize-habits-v3 + "+LEVEL=2&+ORG_GTD=\"Calendar\"" + 'agenda)))) + +(defun org-gtd-upgrades--organize-habits-v3 () + (when (org-is-habit-p) + (setq org-map-continue-from (- (org-element-property :begin + (org-element-at-point)) + 1)) + (org-gtd--refile org-gtd-habit org-gtd-habit-template))) + +(defun org-gtd-upgrades--delegated-item-p () + "Return t if item at point is delegated." + (and (org-entry-get (point) "DELEGATED_TO") + (string-equal (org-entry-get (point) "TODO") org-gtd-wait))) + +(defun org-gtd-upgrades--scheduled-item-p () + "Return t if item at point is SCHEDULED and not a habit." + (and (not (org-is-habit-p)) + (org-get-scheduled-time (point)))) + +(provide 'org-gtd-upgrades) +;;; org-gtd-upgrades.el ends here diff --git a/org-gtd.el b/org-gtd.el index 1fe3b28..68329f8 100644 --- a/org-gtd.el +++ b/org-gtd.el @@ -4,8 +4,8 @@ ;; Author: Aldric Giacomoni ;; Homepage: https://github.com/Trevoke/org-gtd.el -;; Package-Requires: ((emacs "27.1") (org-edna "1.1.2") (f "0.20.0") (org "9.6") (org-agenda-property "1.3.1") (transient "0.3.7")) -;; Package-Version: 2.3.0 +;; Package-Requires: ((emacs "27.2") (org-edna "1.1.2") (f "0.20.0") (org "9.6") (org-agenda-property "1.3.1") (transient "0.3.7")) +;; Package-Version: 3.0.0 ;; This file is not part of GNU Emacs. @@ -38,7 +38,7 @@ ;; Upgrade information is also available therein. ;; ;;; Code: -(defconst org-gtd-version "2.3.0") +(defconst org-gtd-version "3.0.0beta") (require 'subr-x) (require 'cl-lib) @@ -49,28 +49,67 @@ (require 'org-element) (require 'org-agenda-property) (require 'org-edna) -(require 'org-gtd-customize) (require 'org-gtd-core) +(require 'org-gtd-id) +(require 'org-gtd-files) +(require 'org-gtd-horizons) +(require 'org-gtd-areas-of-focus) +(require 'org-gtd-clarify) (require 'org-gtd-delegate) (require 'org-gtd-archive) (require 'org-gtd-capture) -(require 'org-gtd-files) (require 'org-gtd-refile) (require 'org-gtd-projects) (require 'org-gtd-agenda) -(require 'org-gtd-inbox-processing) +(require 'org-gtd-organize) +(require 'org-gtd-process) (require 'org-gtd-mode) +(require 'org-gtd-review) +(require 'org-gtd-oops) +(require 'org-gtd-upgrades) (defvar org-gtd-update-ack "1.0.0" "Set this to the latest version you have upgraded to. You will only see warnings relevant to upgrade steps you must take to go up from -your version to the one installed. Use a version string. For instance: +your version to the one installed. Use a version string. For instance: If org-gtd is 2.0.0, use \"2.0.0\". If org-gtd is 2.3.5, use \"2.3.5\".") +(if (version< org-gtd-update-ack "3.0.0beta") + (lwarn 'org-gtd :warning " + +|--------------------------| +| WARNING: BETA RELEASE | +|--------------------------| + +Thank you for testing the beta release of org-gtd 3.0.0 . +The API is stable unless big breakages are discovered for some reason. + +For a summary/dirty changelog, see a file called `changes-for-3.0.org' in the +repository: `https://github.com/trevoke/org-gtd.el'. + +Important notices involve: +- run `org-gtd-upgrade-v2-to-v3' +- this moves all your habits to a new sub-heading in the default org-gtd file +- it will create this file if you don't have it +- as long you respect the Habits structure, move them where you want + +- some of the key commands have changed, e.g. +- `org-gtd-choose' is now `org-gtd-organize' +- `org-gtd-process-item-hooks' is now `org-gtd-organize-hooks' + +So do review this, and join the discord in the readme to discuss the changes! + +To make this warning go away, add the following setting to your config file +(BEFORE ORG-GTD LOADS) + +(setq org-gtd-update-ack \"3.0.0beta\") + +")) + (if (version< org-gtd-update-ack "2.1.0") (lwarn 'org-gtd :warning " diff --git a/org-gtd.info b/org-gtd.info index 2758886..6682018 100644 --- a/org-gtd.info +++ b/org-gtd.info @@ -62,6 +62,7 @@ Configuring * Required configuration of sub-packages:: * configuration options for org-gtd:: * Recommended key bindings:: +* Sample Doom Emacs Config:: Using Org GTD @@ -462,6 +463,7 @@ File: org-gtd.info, Node: Configuring, Prev: Installing, Up: Setting up Org G * Required configuration of sub-packages:: * configuration options for org-gtd:: * Recommended key bindings:: +* Sample Doom Emacs Config::  File: org-gtd.info, Node: The easy way, Next: Required configuration of sub-packages, Up: Configuring @@ -574,7 +576,7 @@ File: org-gtd.info, Node: configuration options for org-gtd, Next: Recommended to ‘read-string’.  -File: org-gtd.info, Node: Recommended key bindings, Prev: configuration options for org-gtd, Up: Configuring +File: org-gtd.info, Node: Recommended key bindings, Next: Sample Doom Emacs Config, Prev: configuration options for org-gtd, Up: Configuring 1.4.4 Recommended key bindings ------------------------------ @@ -596,6 +598,30 @@ a prefix, i.e.: etc. + +File: org-gtd.info, Node: Sample Doom Emacs Config, Prev: Recommended key bindings, Up: Configuring + +1.4.5 Sample Doom Emacs Config +------------------------------ + +If you are a Doom Emacs user, then your configuration may look something +like this: + + (use-package! org-gtd + :after org + :config + (org-edna-mode) + (setq org-edna-use-inheritance t) + (map! :leader + (:prefix ("d" . "org-gtd") + :desc "Capture" "c" #'org-gtd-capture + :desc "Engage" "e" #'org-gtd-engage + :desc "Process inbox" "p" #'org-gtd-process-inbox + :desc "Show all next" "n" #'org-gtd-show-all-next + :desc "Stuck projects" "s" #'org-gtd-show-stuck-projects)) + (map! :map org-gtd-process-map + :desc "Choose" "C-c c" #'org-gtd-choose)) +  File: org-gtd.info, Node: Using Org GTD, Next: Troubleshooting, Prev: Setting up Org GTD, Up: Top @@ -1021,53 +1047,54 @@ project.  Tag Table: Node: Top736 -Node: Setting up Org GTD2331 -Node: Summary2751 -Node: Upgrading6199 -Node: 220 <- 2106558 -Ref: respect org-mode's org-reverse-note-order variable6684 -Node: 210 <- 2007384 -Ref: Update org-edna trigger7529 -Node: 200 <- 11x8298 -Ref: Configuration8424 -Ref: Example upgrade9816 -Ref: Relevant commands with new names12756 -Ref: heading states (TODO etc)12973 -Ref: Differentiating GTD types of items13132 -Ref: Multiple refile targets14499 -Ref: Key bindings15351 -Node: Installing15628 -Node: use-package15923 -Node: Manually16135 -Node: Configuring16501 -Node: The easy way16757 -Node: Required configuration of sub-packages17326 -Ref: org-edna17574 -Node: configuration options for org-gtd17992 -Ref: I don't care just let me start using it18242 -Ref: Tell me all the levers I can pull18831 -Node: Recommended key bindings21902 -Node: Using Org GTD22630 -Node: Adding things to the inbox23312 -Node: Processing the inbox24127 -Node: Projects26827 -Node: Modify an existing project28163 -Node: Quick action29107 -Node: Trash29522 -Node: Calendar29842 -Node: Delegate30180 -Node: Single action30602 -Node: Archive30908 -Node: Incubate31371 -Node: Engaging with your GTD items31876 -Node: Interacting with org-agenda33013 -Node: Cleaning up / archiving completed work33884 -Node: Multiple files / refile targets34738 -Node: New project heading35081 -Node: Other headings35523 -Node: Troubleshooting36108 -Node: Projects without a NEXT item36326 -Node: I can't create a project when clarifying an inbox item!36996 +Node: Setting up Org GTD2360 +Node: Summary2780 +Node: Upgrading6228 +Node: 220 <- 2106587 +Ref: respect org-mode's org-reverse-note-order variable6713 +Node: 210 <- 2007413 +Ref: Update org-edna trigger7558 +Node: 200 <- 11x8327 +Ref: Configuration8453 +Ref: Example upgrade9845 +Ref: Relevant commands with new names12785 +Ref: heading states (TODO etc)13002 +Ref: Differentiating GTD types of items13161 +Ref: Multiple refile targets14528 +Ref: Key bindings15380 +Node: Installing15657 +Node: use-package15952 +Node: Manually16164 +Node: Configuring16530 +Node: The easy way16815 +Node: Required configuration of sub-packages17384 +Ref: org-edna17632 +Node: configuration options for org-gtd18050 +Ref: I don't care just let me start using it18300 +Ref: Tell me all the levers I can pull18889 +Node: Recommended key bindings21960 +Node: Sample Doom Emacs Config22721 +Node: Using Org GTD23579 +Node: Adding things to the inbox24261 +Node: Processing the inbox25076 +Node: Projects27776 +Node: Modify an existing project29112 +Node: Quick action30056 +Node: Trash30471 +Node: Calendar30791 +Node: Delegate31129 +Node: Single action31551 +Node: Archive31857 +Node: Incubate32320 +Node: Engaging with your GTD items32825 +Node: Interacting with org-agenda33962 +Node: Cleaning up / archiving completed work34833 +Node: Multiple files / refile targets35687 +Node: New project heading36030 +Node: Other headings36472 +Node: Troubleshooting37057 +Node: Projects without a NEXT item37275 +Node: I can't create a project when clarifying an inbox item!37945  End Tag Table diff --git a/test/archiving-test.el b/test/archiving-test.el index ad23952..d21979a 100644 --- a/test/archiving-test.el +++ b/test/archiving-test.el @@ -6,17 +6,19 @@ (require 'buttercup) (require 'with-simulated-input) -(describe "archiving" +(describe + "archiving" - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem)) - (after-each (ogt--close-and-delete-files)) + :var ((inhibit-message t)) - (describe "finished work" + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) - (it "archives completed and canceled projects" - (ogt--add-and-process-project "project headline") + (describe + "finished work" + + (it "archives completed and canceled projects" + (ogt-capture-and-process-project "project headline") (with-current-buffer (org-gtd--default-file) (goto-char (point-max)) (newline) @@ -30,37 +32,37 @@ (expect archived-projects :to-match "completed") (expect archived-projects :to-match "canceled")))) - (it "on a single action" - (ogt--add-and-process-single-action "one") - (ogt--save-all-buffers) - (ogt--add-and-process-single-action "two") - (ogt--save-all-buffers) - (with-current-buffer (org-gtd--default-file) - (goto-char (point-min)) - (search-forward "NEXT one") - (org-todo "DONE")) - (org-gtd-archive-completed-items) - (ogt--save-all-buffers) - (with-current-buffer (org-gtd--default-file) - (expect (buffer-string) - :to-match - "two") - (expect (buffer-string) - :not :to-match - " DONE one"))) + (it "on a single action" + (ogt-capture-and-process-single-action "one") + (ogt--save-all-buffers) + (ogt-capture-and-process-single-action "two") + (ogt--save-all-buffers) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "NEXT one") + (org-todo org-gtd-done)) + (org-gtd-archive-completed-items) + (ogt--save-all-buffers) + (with-current-buffer (org-gtd--default-file) + (expect (buffer-string) + :to-match + "two") + (expect (buffer-string) + :not :to-match + " DONE one"))) - (it "does not archive repeating scheduled items" - (let* ((temporary-file-directory org-gtd-directory) - (gtd-file (make-temp-file "foo" nil ".org" (org-file-contents "test/fixtures/gtd-file.org")))) - (org-gtd-archive-completed-items) - (with-current-buffer (find-file-noselect gtd-file) - (expect (ogt--current-buffer-raw-text) :to-match "repeating item") - (expect (ogt--current-buffer-raw-text) :not :to-match "write a nice test")))) + (it "does not archive repeating scheduled items" + (let* ((temporary-file-directory org-gtd-directory) + (gtd-file (make-temp-file "foo" nil ".org" (org-file-contents "test/fixtures/gtd-file.org")))) + (org-gtd-archive-completed-items) + (with-current-buffer (find-file-noselect gtd-file) + (expect (ogt--current-buffer-raw-text) :to-match "repeating item") + (expect (ogt--current-buffer-raw-text) :not :to-match "write a nice test")))) - (it "does not archive undone incubated items" - (let* ((temporary-file-directory org-gtd-directory) - (gtd-file (make-temp-file "foo" nil ".org" (org-file-contents "test/fixtures/gtd-file.org")))) - (org-gtd-archive-completed-items) - (with-current-buffer (find-file-noselect gtd-file) - (expect (ogt--current-buffer-raw-text) :to-match "For later") - (expect (ogt--current-buffer-raw-text) :not :to-match "not worth thinking about"))))) + (it "does not archive undone incubated items" + (let* ((temporary-file-directory org-gtd-directory) + (gtd-file (make-temp-file "foo" nil ".org" (org-file-contents "test/fixtures/gtd-file.org")))) + (org-gtd-archive-completed-items) + (with-current-buffer (find-file-noselect gtd-file) + (expect (ogt--current-buffer-raw-text) :to-match "For later") + (expect (ogt--current-buffer-raw-text) :not :to-match "not worth thinking about"))))) diff --git a/test/areas-of-focus.el b/test/areas-of-focus.el new file mode 100644 index 0000000..a948db2 --- /dev/null +++ b/test/areas-of-focus.el @@ -0,0 +1,41 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Areas of focus" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs) + (add-hook 'org-gtd-organize-hooks #'org-gtd-set-area-of-focus) + (setq org-gtd-areas-of-focus '("Health" "Home" "Career"))) + (after-each (ogt--close-and-delete-files) + (remove-hook 'org-gtd-organize-hooks #'org-gtd-set-area-of-focus) + (setq org-gtd-areas-of-focus nil)) + + (describe + "org-mode CATEGORY" + + (it "is set on clarified item from a customizable list" + (ogt-capture-single-item "Medical Appointment") + (org-gtd-process-inbox) + (execute-kbd-macro (kbd "C-c c s H e a l t h RET")) + (org-gtd-engage) + (expect (ogt--buffer-string org-agenda-buffer) + :to-match + "Health.*Medical")) + + (it "is set on item at point from the areas of focus decoration" + (with-current-buffer (get-buffer-create "temp.org") + (org-mode) + (insert "* A heading") + (with-simulated-input "Health RET" + (org-gtd-set-area-of-focus)) + (expect (org-entry-get (point) "CATEGORY") + :to-equal + "Health") + (kill-buffer))))) diff --git a/test/calendar-test.el b/test/calendar-test.el new file mode 100644 index 0000000..baa9ef8 --- /dev/null +++ b/test/calendar-test.el @@ -0,0 +1,51 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "A calendar item" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "can be added programmatically" + (org-gtd-calendar-create "Dentist appointment" + (format-time-string "%Y-%m-%d")) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) + :to-match + "Dentist appointment"))) + + (it "has a specific property with the active timestamp" + (let* ((date (calendar-current-date)) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (ogt-capture-and-process-calendar-item "Yowza" date) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Yowza") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-match (format "%s-%#02d-%#02d" year month day))))) + + (describe + "compatibility with orgzly" + (it "has a copy of the active timestamp in the body" + (let* ((date (calendar-current-date)) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (ogt-capture-and-process-calendar-item "Yowza" date) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Yowza") + (org-end-of-meta-data t) + (expect (ogt--current-buffer-raw-text) + :to-match + (format "<%s-%#02d-%#02d>" year month day))))))) diff --git a/test/clarify-test.el b/test/clarify-test.el new file mode 100644 index 0000000..9fca8b7 --- /dev/null +++ b/test/clarify-test.el @@ -0,0 +1,58 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Flow for clarifying items" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "stores a marker to the original heading as local variable in the WIP buffer" + (let ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify"))) + (with-current-buffer source-buffer + (org-gtd-clarify-item)) + + (let ((task-id (with-current-buffer source-buffer (org-id-get))) + (wip-buffer (car (org-gtd-clarify--get-buffers)))) + (with-current-buffer wip-buffer + (expect (org-entry-get org-gtd-clarify--source-heading-marker "ID") + :to-equal task-id))))) + (describe + "through the agenda view" + + (it "handles the target heading" + (ogt-capture-and-process-incubated-item "projectify-me" (calendar-current-date)) + (org-gtd-engage) + (set-buffer org-agenda-buffer) + (goto-char (point-min)) + (search-forward "projectify") + (org-gtd-clarify-agenda-item) + (execute-kbd-macro (kbd "M-> RET")) + (insert ogt--project-text) + (ogt-clarify-as-project) + (kill-buffer org-agenda-buffer) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) + :to-match + "Task 1")) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward ":ORG_GTD: Incubated") + (org-narrow-to-subtree) + (expect (ogt--current-buffer-raw-text) + :not :to-match + "projectify") + (widen) + (search-forward ":ORG_GTD: Projects") + (org-narrow-to-subtree) + (expect (ogt--current-buffer-raw-text) + :to-match + "projectify") + (widen))))) diff --git a/test/delegating-test.el b/test/delegating-test.el index f1ef5ae..7b13dc1 100644 --- a/test/delegating-test.el +++ b/test/delegating-test.el @@ -5,25 +5,70 @@ (require 'buttercup) (require 'with-simulated-input) -(describe "delegating a task" - - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem)) - (after-each (ogt--close-and-delete-files)) - - (it "can be done through the agenda and show on the agenda" - (ogt--add-and-process-single-action "delegateme") - (ogt--save-all-buffers) - (org-gtd-engage) - (with-current-buffer org-agenda-buffer - (goto-char (point-min)) - (search-forward "delegateme") - (with-simulated-input "That SPC Guy RET RET" - (org-gtd-agenda-delegate))) - - (ogt--save-all-buffers) - (org-gtd-engage) - (with-current-buffer org-agenda-buffer - (expect (ogt--current-buffer-raw-text) :to-match "WAIT ") - (expect (ogt--current-buffer-raw-text) :to-match "That Guy")))) +(describe + "delegating a task" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "can be done programmatically" + (org-gtd-delegate-create "Talk to university" + "Favorite student" + (format-time-string "%Y-%m-%d")) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) + :to-match + "Talk to university"))) + + (it "can be done through the agenda and show on the agenda" + (ogt-capture-and-process-single-action "delegateme") + (ogt--save-all-buffers) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (goto-char (point-min)) + (search-forward "delegateme") + (with-simulated-input "That SPC Guy RET RET" + (org-gtd-delegate-agenda-item))) + + (ogt--save-all-buffers) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) :to-match "WAIT ") + (expect (ogt--current-buffer-raw-text) :to-match "That Guy")))) + +(describe + "A delegated item" + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "has a specific property with the active timestamp" + (let* ((date (calendar-current-date)) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (ogt-capture-and-process-delegated-item "TASK DESC" "Someone" date) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "TASK DESC") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-match (format "%s-%#02d-%#02d" year month day))))) + + (describe + "compatibility with orgzly" + (it "has a copy of the active timestamp in the body" + (let* ((date (calendar-current-date)) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (ogt-capture-and-process-delegated-item "TASK DESC" "Someone" date) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "TASK DESC") + (org-end-of-meta-data t) + (expect (buffer-substring (point) (point-max)) + :to-match + (format "<%s-%#02d-%#02d>" year month day))))))) diff --git a/test/files-test.el b/test/files-test.el index 24e281e..eb9e468 100644 --- a/test/files-test.el +++ b/test/files-test.el @@ -6,55 +6,48 @@ (require 'buttercup) (require 'with-simulated-input) -(describe "Create a default file" +(describe + "Create a default file" - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem)) - (after-each (ogt--close-and-delete-files)) + :var ((inhibit-message t)) - (describe "with default content" - (it "for the inbox" - (with-current-buffer (org-gtd--inbox-file) + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (describe + "with default content" + (it "for the inbox" + (with-current-buffer (ogt-inbox-buffer) (expect (ogt--current-buffer-raw-text) :to-match "This is the inbox") (expect (ogt--current-buffer-raw-text) :to-match - "#\\+STARTUP: overview hidestars logrefile indent logdone"))) - - (it "has a header for the default file" - (with-current-buffer (org-gtd--default-file) - (expect (ogt--current-buffer-raw-text) - :to-match - "#\\+STARTUP: overview indent align inlineimages hidestars logdone logrepeat logreschedule logredeadline -#\\+TODO: NEXT(n) TODO(t) WAIT(w@) | DONE(d) CNCL(c@)"))) - - - - (describe - "when there isn't a refile target" - (it "for a project" - (ogt--add-and-process-project "project headline") - (ogt--save-all-buffers) - (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) - - (it "for a calendar item" - (ogt--add-and-process-calendar-item "calendar headline") - (ogt--save-all-buffers) - (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) - - (it "for a delegated item" - (ogt--add-and-process-delegated-item "delegated headline") - (ogt--save-all-buffers) - (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) - - (it "for a incubated item" - (ogt--add-and-process-incubated-item "incubated headline") - (ogt--save-all-buffers) - (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) - - (it "for a single action" - (ogt--add-and-process-single-action "single action") - (ogt--save-all-buffers) - (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org"))))) + "This is the inbox."))) + + (describe + "when there isn't a refile target" + (it "for a project" + (ogt-capture-and-process-project "project headline") + (ogt--save-all-buffers) + (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) + + (it "for a calendar item" + (ogt-capture-and-process-calendar-item "calendar headline") + (ogt--save-all-buffers) + (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) + + (it "for a delegated item" + (ogt-capture-and-process-delegated-item "delegated-headline") + (ogt--save-all-buffers) + (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) + + (it "for a incubated item" + (ogt-capture-and-process-incubated-item "incubated headline") + (ogt--save-all-buffers) + (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org")) + + (it "for a single action" + (ogt-capture-and-process-single-action "single action") + (ogt--save-all-buffers) + (expect (ogt--org-dir-buffer-string) :to-match "org-gtd-tasks\\.org"))))) diff --git a/test/fixtures/areas-of-focus.org b/test/fixtures/areas-of-focus.org new file mode 100644 index 0000000..eac9caf --- /dev/null +++ b/test/fixtures/areas-of-focus.org @@ -0,0 +1,107 @@ +#+STARTUP: overview indent align inlineimages hidestars + +* Projects +:PROPERTIES: +:TRIGGER: org-gtd-next-project-action org-gtd-update-project-task! +:ORG_GTD: Projects +:END: + +** [0/3] Fix the roof +:PROPERTIES: +:CATEGORY: Home +:END: +*** NEXT Clean gutters + +*** TODO Replace broken tiles + +*** TODO Get cat trampoline + + +** [1/3] Get promotion +:PROPERTIES: +:CATEGORY: Career +:END: +*** DONE Make list of accomplishments +*** NEXT Figure out desired raise +*** TODO Kidnap boss + +** [3/3] Get pregnant +:PROPERTIES: +:CATEGORY: Health +:END: +*** DONE Get doctor's appointment +*** CNCL Find partner +*** CNCL Have child +** [1/3] Be happy in kitchen +:PROPERTIES: +:CATEGORY: Home +:END: +*** DONE Throw away boxes +*** NEXT think of something that will make me enjoy being in the kitchen +*** TODO Make the change and be happy +* Calendar +:PROPERTIES: +:ORG_GTD: Calendar +:END: + +** Meet plumber +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2021-11-20 10:00-12:00 Sat> +:CATEGORY: Home +:END: + +<2021-11-20 10:00-12:00 Sat> +** some scheduled item +SCHEDULED: <2021-11-20 Sat> +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2021-11-18 Thu> +:END: + +** Sweep the floor +SCHEDULED: <2021-11-20 9:00-9:15 .+1d> +:PROPERTIES: +:STYLE: habit +:CATEGORY: Home +:END: +* Actions +:PROPERTIES: +:ORG_GTD: Actions +:END: + +** NEXT Open mail +:PROPERTIES: +:CATEGORY: Home +:END: + +** NEXT Update resume +:PROPERTIES: +:CATEGORY: Career +:END: + + +** WAIT Wait for someone + +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2025-03-23 Sun> +:DELEGATED_TO: future me +:END: + +- State "WAIT" from "NEXT" [2021-11-20 Sat 16:42] \\ + wait for me +** DONE this is done +** CNCL this is canceled + +* Incubate +:PROPERTIES: +:ORG_GTD: Incubated +:END: +** For later +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2037-02-19 Thu> +:CATEGORY: Home +:END: + +** CNCL not worth thinking about +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2021-11-21 Sun> +:END: diff --git a/test/fixtures/gtd-file.org b/test/fixtures/gtd-file.org index ee3dbd6..6d9b47d 100644 --- a/test/fixtures/gtd-file.org +++ b/test/fixtures/gtd-file.org @@ -1,13 +1,15 @@ -#+STARTUP: overview indent align inlineimages hidestars logdone logrepeat logreschedule logredeadline -#+TODO: NEXT(n) TODO(t) WAIT(w@) | DONE(d) CNCL(c@) +#+STARTUP: overview indent align inlineimages hidestars * Projects :PROPERTIES: -:TRIGGER: relatives(forward-no-wrap todo-only 1 no-sort) todo!(NEXT) +:TRIGGER: org-gtd-next-project-action org-gtd-update-project-task! :ORG_GTD: Projects :END: ** [0/3] Unstarted project +:PROPERTIES: +:CATEGORY: Home +:END: *** NEXT Task 1 *** TODO Task 2 @@ -16,15 +18,24 @@ ** [1/3] cancel me +:PROPERTIES: +:CATEGORY: Career +:END: *** DONE Task 1 *** NEXT Task 2 *** TODO Task 3 ** [3/3] canceled +:PROPERTIES: +:CATEGORY: Health +:END: *** DONE Task 1 *** CNCL Task 2 *** CNCL Task 3 ** [1/3] addtaskhere +:PROPERTIES: +:CATEGORY: Home +:END: *** DONE finished task *** NEXT initial next task *** TODO initial last task @@ -34,13 +45,19 @@ :END: ** DONE write a nice test -SCHEDULED: <2021-11-20 Sat> +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2021-11-20 Sat> +:CATEGORY: Home +:END: + ** probably overdue by now -SCHEDULED: <2021-11-18 Thu> +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2021-11-18 Thu> +:END: ** repeating item -SCHEDULED: <2021-12-04 Sat> +SCHEDULED: <2021-12-04 Sat .+1d> :PROPERTIES: :STYLE: habit :LAST_REPEAT: [2021-11-20 Sat 16:52] @@ -55,9 +72,17 @@ SCHEDULED: <2021-12-04 Sat> :END: ** NEXT Do this soon +:PROPERTIES: +:CATEGORY: Home +:END: ** WAIT Wait for someone +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2025-03-23 Sun> +:DELEGATED_TO: future me +:END: + - State "WAIT" from "NEXT" [2021-11-20 Sat 16:42] \\ wait for me ** DONE this is done @@ -68,6 +93,12 @@ SCHEDULED: <2021-12-04 Sat> :ORG_GTD: Incubated :END: ** For later -SCHEDULED: <2037-02-19 Thu> +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2037-02-19 Thu> +:CATEGORY: Home +:END: + ** CNCL not worth thinking about -SCHEDULED: <2021-11-21 Sun> +:PROPERTIES: +:ORG_GTD_TIMESTAMP: <2021-11-21 Sun> +:END: diff --git a/test/habits-test.el b/test/habits-test.el new file mode 100644 index 0000000..5e46c3d --- /dev/null +++ b/test/habits-test.el @@ -0,0 +1,35 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "A habit" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "can be added programmatically" + (org-gtd-habit-create "Dentist appointment" + ".+3m") + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) + :to-match + "Dentist appointment"))) + + + (it "is formatted like org-mode wants" + (let* ((repeater "++1m")) + (ogt-capture-and-process-habit "Yowza" repeater) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Yowza") + (expect (org-entry-get (point) "STYLE") + :to-equal "habit") + (expect (org-entry-get (point) "SCHEDULED") + :to-match (format "%s" repeater)))))) diff --git a/test/helpers/clarifying.el b/test/helpers/clarifying.el new file mode 100644 index 0000000..271b56c --- /dev/null +++ b/test/helpers/clarifying.el @@ -0,0 +1,45 @@ +(defun ogt-clarify-as-single-action () + (let ((inhibit-message t)) + (execute-kbd-macro (kbd "C-c c s")))) + +(defun ogt-clarify-as-project () + (let ((inhibit-message t)) + (execute-kbd-macro (kbd "C-c c p")))) + +(defun ogt-clarify-as-incubated-item (&optional date) + (let ((inhibit-message t)) + (let* ((date (or date (calendar-current-date))) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (execute-kbd-macro (kbd "C-c c i %s-%s-%s RET"))))) + +(defun ogt-clarify-as-delegated-item (&optional to-whom date) + (let ((inhibit-message t)) + (let* ((person (or to-whom "Someone")) + (date (or date (calendar-current-date))) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (execute-kbd-macro (kbd (format "C-c c d %s RET %s-%s-%s RET" to-whom year month day)))))) + +(defun ogt-clarify-as-calendar-item (&optional date) + (let ((inhibit-message t)) + (let* ((date (or date (calendar-current-date))) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (execute-kbd-macro (kbd (format "C-c c c %s-%s-%s RET" year month day)))))) + +(defun ogt-clarify-as-habit (repeater) + (let ((inhibit-message t)) + (execute-kbd-macro (kbd (format "C-c c h %s RET" repeater))))) + +(defun ogt-clarify-as-knowledge-item () + (let ((inhibit-message t)) + (execute-kbd-macro (kbd "C-c c k")))) + + +(defun ogt-clarify-as-trash-item () + (let ((inhibit-message t)) + (execute-kbd-macro (kbd "C-c c t")))) diff --git a/test/helpers/processing.el b/test/helpers/processing.el index d7d1f99..37da8ee 100644 --- a/test/helpers/processing.el +++ b/test/helpers/processing.el @@ -1,36 +1,72 @@ -(defun ogt--add-single-item (&optional label) - (org-gtd-capture nil "i") - (insert (or label "single action")) - (org-capture-finalize)) +(load "test/helpers/clarifying.el") -(defun ogt--add-and-process-project (label) +(defun ogt-capture-single-item (&optional label) + (let ((inhibit-message t)) + (org-gtd-capture nil "i") + (insert (or label "single action")) + (org-capture-finalize))) + +(defun ogt-capture-and-process-project (label) "LABEL is the project label." - (ogt--add-single-item label) - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "M-> RET")) - (insert ogt--project-text) - (execute-kbd-macro (kbd "C-c c p TAB RET"))) - -(defun ogt--add-and-process-calendar-item (label) - "LABEL is the calendared item label." - (ogt--add-single-item label) - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c c RET TAB RET"))) - -(defun ogt--add-and-process-delegated-item (label) - "LABEL is the delegated label." - (ogt--add-single-item label) - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c d RET Someone RET TAB RET"))) - -(defun ogt--add-and-process-incubated-item (label) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (execute-kbd-macro (kbd "M-> RET")) + (with-current-buffer (current-buffer) + (insert ogt--project-text)) + (ogt-clarify-as-project))) + +(defun ogt-capture-and-process-calendar-item (label &optional date) + "DATE has to be like the output of `calendar-current-date' so (MM DD YYYY)." + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-calendar-item date))) + +(defun ogt-capture-and-process-habit (label repeater) + "REPEATER is an org-mode date repeater, e.g. .+1d or ++1m, etc." + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-habit repeater))) + +(defun ogt-capture-and-process-delegated-item (label &optional to-whom date) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-delegated-item to-whom date))) + +(defun ogt-capture-and-process-incubated-item (label &optional date) "LABEL is the incubated label." - (ogt--add-single-item label) - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c i RET TAB RET"))) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-incubated-item date))) -(defun ogt--add-and-process-single-action (label) +(defun ogt-capture-and-process-single-action (label) "LABEL is the single action label." - (ogt--add-single-item label) - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c s TAB RET"))) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-single-action))) + +(defun ogt-capture-and-process-knowledge-item (label) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-knowledge-item))) + +(defun ogt-capture-and-process-addition-to-project (label project-heading-simulated-input) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + + (with-simulated-input project-heading-simulated-input + (org-gtd-organize--call + org-gtd-organize-add-to-project-func)))) + +(defun ogt-capture-and-process-trash-item (label) + (let ((inhibit-message t)) + (ogt-capture-single-item label) + (org-gtd-process-inbox) + (ogt-clarify-as-trash-item))) diff --git a/test/helpers/project.el b/test/helpers/project-fixtures.el similarity index 84% rename from test/helpers/project.el rename to test/helpers/project-fixtures.el index c8a185f..fc11aac 100644 --- a/test/helpers/project.el +++ b/test/helpers/project-fixtures.el @@ -1,7 +1,8 @@ (defconst ogt--base-project-heading "* AdditionalHeading :PROPERTIES: -:ORG_GTD: Projects +:TRIGGER: org-gtd-next-project-action org-gtd-update-project-task! +:ORG_GTD: Projects :END: ") diff --git a/test/helpers/setup.el b/test/helpers/setup.el index e27b466..7843416 100644 --- a/test/helpers/setup.el +++ b/test/helpers/setup.el @@ -1,23 +1,19 @@ -(load "test/helpers/project.el") +(setq org-gtd-update-ack "2.1.0") + +(load "test/helpers/clarifying.el") +(load "test/helpers/project-fixtures.el") (load "test/helpers/processing.el") (load "test/helpers/utils.el") (defun ogt--configure-emacs () - (setq org-gtd-directory (make-temp-file "org-gtd" t) - org-gtd-process-item-hooks '() - org-gtd-refile-to-any-target nil + (setq last-command nil + org-gtd-directory (make-temp-file "org-gtd" t) + org-gtd-areas-of-focus nil + org-gtd-organize-hooks '() + org-gtd-refile-to-any-target t org-edna-use-inheritance t) (org-edna-mode 1) - (define-key org-gtd-process-map (kbd "C-c c") #'org-gtd-choose)) - -(defun ogt--prepare-filesystem () - "run before each test" - ;(ogt--clean-target-directory org-gtd-directory) - ) - -(defun ogt--clean-target-directory (dir) - (delete-directory dir t nil) - (make-directory dir)) + (define-key org-gtd-clarify-map (kbd "C-c c") #'org-gtd-organize)) (defun ogt--reset-var (symbl) "Reset SYMBL to its standard value." @@ -25,10 +21,13 @@ (defun ogt--close-and-delete-files () "Run after every test to clear open buffers state" - (kill-matching-buffers ".*\\.org" nil t) - (kill-matching-buffers ".*Agenda.*" nil t) - (kill-matching-buffers ".*Calendar.*" nil t) - ) + + (mapc + #'ogt--kill-buffer + (-flatten (mapcar + #'ogt--get-buffers + `(".*\\.org" ".*Agenda.*" "gtd_archive.*" ".*Calendar.*" + ,(format ".*%s.*" org-gtd-clarify--prefix)))))) (defun ogt--clear-file-and-buffer (buffer) (if (bufferp buffer) @@ -36,3 +35,14 @@ (with-current-buffer buffer (basic-save-buffer)) (kill-buffer buffer) (delete-file filename)))) + +(defun ogt--get-buffers (regexp) + (seq-filter (lambda (buf) + (string-match-p regexp (buffer-name buf))) + (buffer-list))) + +(defun ogt--kill-buffer (buffer) + (when (buffer-file-name buffer) + (with-current-buffer buffer + (revert-buffer t t))) + (kill-buffer buffer)) diff --git a/test/helpers/utils.el b/test/helpers/utils.el index de10170..c6faaca 100644 --- a/test/helpers/utils.el +++ b/test/helpers/utils.el @@ -1,13 +1,12 @@ -(defun ogt--org-dir-buffer-string () - (let ((ogt-files (progn (list-directory org-gtd-directory) - (with-current-buffer "*Directory*" - (buffer-string))))) - (kill-buffer "*Directory*") - ogt-files)) +(defun create-additional-project-target (filename) + (ogt--create-org-file-in-org-gtd-dir filename ogt--base-project-heading)) + +(defun ogt-inbox-buffer () + (find-file-noselect (org-gtd-inbox-path))) (defun ogt--archive () "Create or return the buffer to the archive file." - (with-current-buffer (org-gtd--inbox-file) + (with-current-buffer (ogt-inbox-buffer) (find-file-noselect (car (with-org-gtd-context (org-archive--compute-location @@ -18,13 +17,22 @@ (ogt--buffer-string (ogt--archive))) (defun ogt--save-all-buffers () - (with-simulated-input "!" (save-some-buffers))) + (let ((inhibit-message t)) + (with-simulated-input "!" (save-some-buffers)))) -(defun create-additional-project-target (filename) - (let* ((file (f-join org-gtd-directory (format "%s.org" filename))) +(defun ogt--temp-org-file-buffer (basename &optional text) + "Create a new org-mode file with a unique name. +The name is based on BASENAME, the TEXT is optional content. +Return the buffer visiting that file." + (let ((filename (make-temp-file basename nil ".org" text))) + (find-file-noselect filename))) + +(defun ogt--create-org-file-in-org-gtd-dir (basename &optional initial-contents) + (let* ((file (f-join org-gtd-directory (string-join `(,basename ".org")))) (buffer (find-file-noselect file))) (with-current-buffer buffer - (insert ogt--base-project-heading) + (org-mode) + (insert (or initial-contents "")) (basic-save-buffer)) buffer)) @@ -36,3 +44,15 @@ (defun ogt--current-buffer-raw-text () "Returns text without faces" (buffer-substring-no-properties (point-min) (point-max))) + +(defun ogt--print-buffer-list () + (message "*** Start List of active buffers") + (mapc (lambda (x) (message (buffer-name x))) (buffer-list)) + (message "*** End List of active buffers")) + +(defun ogt--org-dir-buffer-string () + (let ((ogt-files (progn (list-directory org-gtd-directory) + (with-current-buffer "*Directory*" + (buffer-string))))) + (kill-buffer "*Directory*") + ogt-files)) diff --git a/test/horizons-test.el b/test/horizons-test.el new file mode 100644 index 0000000..fd704df --- /dev/null +++ b/test/horizons-test.el @@ -0,0 +1,75 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(load "test/helpers/utils.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Higher horizons" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (describe + "when org-gtd should show the horizons" + + (before-each (setq org-gtd-clarify-show-horizons 'right)) + (after-each (setq org-gtd-clarify-show-horizons nil)) + + (describe + "when we clarify an item" + + (it "creates a templated file when there isn't one" + (ogt-capture-single-item "Add a configuration option") + (org-gtd-process-inbox) + (expect (get-buffer-window "horizons.org") + :not :to-be + nil) + (expect (get-buffer-window (car (org-gtd-clarify--get-buffers))) + :not :to-be + nil)) + + (it "shows the existing file if there is one" + (ogt--create-org-file-in-org-gtd-dir + "horizons" + "We are the champions") + (ogt-capture-single-item "Add a configuration option") + (org-gtd-process-inbox) + (expect (get-buffer-window "horizons.org") + :not :to-be + nil) + (expect (get-buffer-window (car (org-gtd-clarify--get-buffers))) + :not :to-be + nil) + (expect (ogt--buffer-string "horizons.org") + :to-match + "We are the champions")) + + (it "when we return to a WIP buffer" + (ogt-capture-single-item "Add a configuration option") + (org-gtd-process-inbox) + (set-buffer "*scratch*") + (delete-other-windows) + (with-simulated-input + "TAB RET" + (org-gtd-clarify-switch-to-buffer)) + (expect (get-buffer-window "horizons.org") + :not :to-be + nil)))) + + + (describe + "when org-gtd should not show the horizons" + (before-each (setq org-gtd-clarify-show-horizons nil)) + (after-each (setq org-gtd-clarify-show-horizons nil)) + + (it "does not show the window" + (ogt-capture-single-item "Add a configuration option") + (org-gtd-process-inbox) + (expect (get-buffer-window "horizons.org") + :to-be + nil)))) diff --git a/test/incubate-test.el b/test/incubate-test.el new file mode 100644 index 0000000..9a485c8 --- /dev/null +++ b/test/incubate-test.el @@ -0,0 +1,55 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "An incubated item" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "has a specific property with the active timestamp" + (let* ((date (calendar-current-date)) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (ogt-capture-and-process-incubated-item "Yowza" date) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Yowza") + (expect (org-entry-get (point) "ORG_GTD" t) + :to-equal org-gtd-incubate) + (expect (org-entry-get (point) org-gtd-timestamp) + :to-match (format "%s-%#02d-%#02d" year month day))))) + + (it "can be added programmatically" + (org-gtd-incubate-create "Dentist appointment" + (format-time-string "%Y-%m-%d")) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) + :to-match + "Dentist appointment"))) + + + (describe + "compatibility with orgzly" + + (it "has a copy of the active timestamp in the body" + (let* ((date (calendar-current-date)) + (year (nth 2 date)) + (month (nth 0 date)) + (day (nth 1 date))) + (ogt-capture-and-process-incubated-item "Yowza" date) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Yowza") + (org-end-of-meta-data t) + (expect (buffer-substring (point) (point-max)) + :to-match + (format "<%s-%#02d-%#02d>" year month day))))))) diff --git a/test/knowledge-test.el b/test/knowledge-test.el new file mode 100644 index 0000000..edbfe5b --- /dev/null +++ b/test/knowledge-test.el @@ -0,0 +1,19 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Processing a knowledge item" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "through the inbox, moves the task to the archive file" + (ogt-capture-and-process-knowledge-item "Yowza") + (with-current-buffer (ogt--archive) + (expect (buffer-string) :to-match "Yowza")))) diff --git a/test/org-gtd-core-test.el b/test/org-gtd-core-test.el new file mode 100644 index 0000000..e8cedc9 --- /dev/null +++ b/test/org-gtd-core-test.el @@ -0,0 +1,44 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(load "test/helpers/utils.el") +(require 'org-gtd) +(require 'buttercup) + +(describe + "org-gtd-agenda-files" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "appends to the existing org-agenda-files" + (let ((org-agenda-files '("/tmp/foo.org"))) + (with-org-gtd-context + (expect org-agenda-files + :to-have-same-items-as + `(,org-gtd-directory "/tmp/foo.org"))))) + + (it "expands and appends if org-agenda-files is a single file" + (let* ((other-dir (make-temp-file "other-dir" t)) + ;(file1 (buffer-file-name (find-file-noselect (f-join other-dir "file1.org")))) + (index (find-file-noselect (f-join other-dir "index")))) + (write-region (f-join other-dir "file1.org") nil (buffer-file-name index)) + ;(with-current-buffer file1 (basic-save-buffer)) + (with-current-buffer index (basic-save-buffer)) + + (let ((org-agenda-files (buffer-file-name index))) + (with-org-gtd-context + (expect org-agenda-files + :to-have-same-items-as + `(,org-gtd-directory ,(f-join other-dir "file1.org"))))) + (kill-buffer index))) + + (it "sets the variable if org-agenda-files is nil" + (let ((org-agenda-files nil)) + (with-org-gtd-context + (expect org-agenda-files + :to-have-same-items-as + `(,org-gtd-directory))))) +) diff --git a/test/organizing-test.el b/test/organizing-test.el new file mode 100644 index 0000000..3fe8df5 --- /dev/null +++ b/test/organizing-test.el @@ -0,0 +1,95 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Organizing (in 3.0)" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (describe + "cleanup" + + :var ((inhibit-message t)) + + (before-each (defun hook1 () + (if (org-gtd-organize-type-member-p '(quick-action)) + (org-entry-put (point) "HOOK1" "YES"))) + (defun hook2 () + (if (org-gtd-organize-type-member-p '(single-action)) + (org-entry-put (point) "HOOK2" "YES")))) + (after-each (fmakunbound 'hook1) + (fmakunbound 'hook2)) + + (it "restores the window configuration" + (let ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify")) + (window-config nil) + (org-gtd-refile-to-any-target t)) + (set-buffer source-buffer) + (org-gtd-clarify-item) + (setq window-config org-gtd-clarify--window-config) + (org-gtd-single-action) + + (expect (compare-window-configurations (current-window-configuration) window-config) + :to-be t))) + + (it "kills the temp buffer" + (let ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify")) + (org-gtd-refile-to-any-target t)) + (set-buffer source-buffer) + (org-gtd-clarify-item) + (org-gtd-single-action) + (expect (org-gtd-clarify--get-buffers) :to-be nil))) + + (it "deletes the source heading" + (let ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify")) + (org-gtd-refile-to-any-target t)) + (set-buffer source-buffer) + (org-gtd-clarify-item) + (org-gtd-single-action) + (expect (buffer-size) :to-equal 0))) + + (it "triggers only the relevant hooks" + (let* ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify")) + (org-gtd-refile-to-any-target t) + (org-gtd-organize-hooks '(hook1 hook2))) + (set-buffer source-buffer) + (org-gtd-clarify-item) + (ogt-clarify-as-single-action) + (with-current-buffer (org-gtd--default-file) + (expect (ogt--current-buffer-raw-text) + :to-match "HOOK2")) + ))) + + (describe + "hook filter helper" + + (it "treats a single argument properly as a list" + (expect (org-gtd-organize-type-member-p 'everything) + :to-be-truthy)) + + (it "is truthy as long as 'everything is in the list" + (expect (org-gtd-organize-type-member-p '(incubated trash everything project-task)) + :to-be-truthy)) + + (it "signals an error if any element in the list is not one of the expected members" + (expect (org-gtd-organize-type-member-p '(foobar)) + :to-throw 'org-gtd-invalid-organize-action-type-error)) + + (it "is truthy if the buffer-local variable is in the list" + (with-temp-buffer + (setq-local org-gtd--organize-type 'quick-action) + (expect (org-gtd-organize-type-member-p '(incubated quick-action delegated)) + :to-be-truthy))) + + (it "is falsey if the buffer-local variable is not the list" + (with-temp-buffer + (setq-local org-gtd--organize-type 'trash) + (expect (org-gtd-organize-type-member-p '(incubated quick-action delegated)) + :not :to-be-truthy))))) diff --git a/test/processing-test.el b/test/processing-test.el index f11b909..44eb624 100644 --- a/test/processing-test.el +++ b/test/processing-test.el @@ -6,95 +6,89 @@ (require 'buttercup) (require 'with-simulated-input) -(describe "Processing items" - - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem) - (ogt--add-single-item)) - (after-each (ogt--close-and-delete-files)) - - (it "processes all the elements" - (dotimes (x 8) - (ogt--add-single-item (format "single action %s" x))) - - (org-gtd-process-inbox) - - (execute-kbd-macro (kbd "M-> RET")) - (insert ogt--project-text) - (execute-kbd-macro (kbd "C-c c p TAB RET")) - - (execute-kbd-macro (kbd "C-c c c RET TAB RET")) - - (execute-kbd-macro (kbd "C-c c d RET Someone RET TAB RET")) - - (execute-kbd-macro (kbd "C-c c i RET TAB RET")) - - (execute-kbd-macro (kbd "C-c c s TAB RET")) - - (execute-kbd-macro (kbd "C-c c s TAB RET")) - - (execute-kbd-macro (kbd "M-> RET")) - (insert ogt--project-text) - (execute-kbd-macro (kbd "C-c c p TAB RET")) - - (execute-kbd-macro (kbd "C-c c c RET TAB RET")) - - (dotimes (x 8) - (ogt--add-single-item (format "single action %s" x))) - - (org-gtd-process-inbox) - - (execute-kbd-macro (kbd "C-c c i RET TAB RET")) - - (execute-kbd-macro (kbd "M-> RET")) - (insert ogt--project-text) - (execute-kbd-macro (kbd "C-c c p TAB RET")) - - (execute-kbd-macro (kbd "C-c c s TAB RET")) - - (execute-kbd-macro (kbd "C-c c s TAB RET")) - - (execute-kbd-macro (kbd "C-c c c RET TAB RET")) - - (execute-kbd-macro (kbd "C-c c d RET Someone RET TAB RET")) - - (execute-kbd-macro (kbd "C-c c i RET TAB RET")) - - (execute-kbd-macro (kbd "C-c c s TAB RET")) - - (execute-kbd-macro (kbd "C-c c s TAB RET")) - - (with-current-buffer (org-gtd--inbox-file) - (expect (ogt--current-buffer-raw-text) - :not :to-match - "single action"))) - - (it "uses configurable decorations on the processed items" - (let ((org-gtd-process-item-hooks '(org-set-tags-command org-priority))) - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c s RET A TAB RET"))) - - (org-gtd-engage) - (let ((ogt-agenda-string (ogt--buffer-string org-agenda-buffer))) - (expect (string-match "NEXT \\[#A\\] single action" ogt-agenda-string) - :to-be-truthy))) - - (it "shows item in agenda when done" - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c s TAB RET")) - (expect (buffer-modified-p (org-gtd--default-file)) :to-equal t) - - (org-gtd-engage) - (let ((ogt-agenda-string (ogt--buffer-string org-agenda-buffer))) - (expect (string-match "single action" ogt-agenda-string) - :to-be-truthy))) - - (describe "error management" - (describe "when project has incorrect shape" - (it "tells the user and returns to editing" - (org-gtd-process-inbox) - (execute-kbd-macro (kbd "C-c c p")) - (expect (buffer-name) :to-match "inbox") - (with-current-buffer "*Message*" - (expect (ogt--current-buffer-raw-text) :to-match "First task")))))) +(describe + "Processing items" + + :var ((inhibit-message t)) + + (before-each + (ogt--configure-emacs) + (ogt-capture-single-item)) + (after-each (ogt--close-and-delete-files)) + + (it "processes all the elements" + (dotimes (x 8) + (ogt-capture-single-item (format "single action %s" x))) + + (org-gtd-process-inbox) + + (execute-kbd-macro (kbd "M-> RET")) + (insert ogt--project-text) + (ogt-clarify-as-project) + + (ogt-clarify-as-calendar-item) + (ogt-clarify-as-delegated-item "Someone") + (ogt-clarify-as-incubated-item) + (ogt-clarify-as-single-action) + (ogt-clarify-as-knowledge-item) + + (execute-kbd-macro (kbd "M-> RET")) + (insert ogt--project-text) + (ogt-clarify-as-project) + + (ogt-clarify-as-calendar-item) + + (dotimes (x 8) + (ogt-capture-single-item (format "single action %s" x))) + + (org-gtd-process-inbox) + + (execute-kbd-macro (kbd "C-c c i RET")) + + (execute-kbd-macro (kbd "M-> RET")) + (insert ogt--project-text) + (ogt-clarify-as-project) + + (ogt-clarify-as-single-action) + (ogt-clarify-as-single-action) + (ogt-clarify-as-calendar-item) + (ogt-clarify-as-delegated-item "Someone") + (ogt-clarify-as-incubated-item) + (ogt-clarify-as-single-action) + (ogt-clarify-as-knowledge-item) + + (with-current-buffer (ogt-inbox-buffer) + (expect (ogt--current-buffer-raw-text) + :not :to-match + "single action"))) + + (it "uses configurable decorations on the processed items" + (let ((org-gtd-organize-hooks '(org-set-tags-command org-priority))) + (org-gtd-process-inbox) + (execute-kbd-macro (kbd "C-c c s RET A TAB RET"))) + + (org-gtd-engage) + (let ((ogt-agenda-string (ogt--buffer-string org-agenda-buffer))) + (expect (string-match "NEXT \\[#A\\] single action" ogt-agenda-string) + :to-be-truthy))) + + (it "shows item in agenda when done" + (org-gtd-process-inbox) + (execute-kbd-macro (kbd "C-c c s TAB RET")) + (expect (buffer-modified-p (org-gtd--default-file)) :to-equal t) + + (org-gtd-engage) + (let ((ogt-agenda-string (ogt--buffer-string org-agenda-buffer))) + (expect (string-match "single action" ogt-agenda-string) + :to-be-truthy))) + + (describe + "error management" + (describe + "when project has incorrect shape" + (it "tells the user and returns to editing" + (org-gtd-process-inbox) + (execute-kbd-macro (kbd "C-c c p RET")) + + (expect (buffer-name) :to-match org-gtd-clarify--prefix) + (expect (ogt--buffer-string "*Message*") :to-match "** First task"))))) diff --git a/test/project-modification-test.el b/test/project-modification-test.el index 1e89843..bd6dc10 100644 --- a/test/project-modification-test.el +++ b/test/project-modification-test.el @@ -8,89 +8,84 @@ (describe "Modifying a project" - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem)) + :var ((inhibit-message t)) + (before-each (ogt--configure-emacs)) (after-each (ogt--close-and-delete-files)) - (describe "when org-reverse-note-order is t" - - (before-each (setq org-reverse-note-order t)) - (after-each (ogt--reset-var 'org-reverse-note-order)) - - (it "as the first NEXT task" - (ogt--add-and-process-project "project headline") - (ogt--add-single-item "Task 0") - (org-gtd-process-inbox) - - (with-simulated-input "project SPC headline TAB RET" - (org-gtd--modify-project)) - (org-gtd-engage) - (with-current-buffer org-agenda-this-buffer-name - (expect (ogt--current-buffer-raw-text) :to-match "Task 0") - (expect (ogt--current-buffer-raw-text) :not :to-match "Task 1"))) - - (it "keeps sanity of TODO states in modified project" - (let* ((temporary-file-directory org-gtd-directory) - (gtd-file (make-temp-file "foo" nil ".org" (org-file-contents "test/fixtures/gtd-file.org")))) - (ogt--add-single-item "Task 0") - (org-gtd-process-inbox) - (with-simulated-input "[1/3] SPC addtaskhere RET" - (org-gtd--modify-project)) - (with-current-buffer (find-file-noselect gtd-file) - (expect (ogt--current-buffer-raw-text) :to-match "NEXT Task 0") - (expect (ogt--current-buffer-raw-text) :to-match "DONE finished task") - (expect (ogt--current-buffer-raw-text) :to-match "TODO initial next task") - (expect (ogt--current-buffer-raw-text) :to-match "TODO initial last task") - - (search-forward "NEXT Task 0") - (org-todo 'done) - (expect (ogt--current-buffer-raw-text) :to-match "DONE Task 0") - (expect (ogt--current-buffer-raw-text) :to-match "DONE finished task") - (expect (ogt--current-buffer-raw-text) :to-match "NEXT initial next task") - (expect (ogt--current-buffer-raw-text) :to-match "TODO initial last task"))))) - - (describe "when org-reverse-note-order is nil" - - (before-each (setq org-reverse-note-order nil)) - (after-each (ogt--reset-var 'org-reverse-note-order)) - - (it "as the last NEXT task" - (ogt--add-and-process-project "project headline") - (ogt--add-single-item "Task 0") - (org-gtd-process-inbox) - - (with-simulated-input "project SPC headline TAB RET" - (org-gtd--modify-project)) - (org-gtd-engage) - (with-current-buffer org-agenda-this-buffer-name - (expect (ogt--current-buffer-raw-text) :not :to-match "Task 0") - (expect (ogt--current-buffer-raw-text) :to-match "Task 1")))) + (describe + "when org-reverse-note-order is t" + + (before-each (setq org-reverse-note-order t)) + (after-each (ogt--reset-var 'org-reverse-note-order)) + + (it "as the first NEXT task" + (ogt-capture-and-process-project "project headline") + (ogt-capture-and-process-addition-to-project "Task 0" "project SPC headline TAB RET") + (org-gtd-engage) + (with-current-buffer org-agenda-this-buffer-name + (goto-char (point-min)) + + (search-forward "Task 0") + (expect (ogt--current-buffer-raw-text) :not :to-match "Task 1"))) + + (it "keeps sanity of TODO states in modified project" + (let* ((temporary-file-directory org-gtd-directory) + (gtd-file-buffer (ogt--temp-org-file-buffer "foo" (org-file-contents "test/fixtures/gtd-file.org")))) + (org-gtd-core-prepare-buffer gtd-file-buffer) + (ogt-capture-and-process-addition-to-project "Task 0" "[1/3] SPC addtaskhere RET") + (with-current-buffer gtd-file-buffer + (goto-char (point-min)) + + (search-forward "NEXT Task 0") + (search-forward "DONE finished task") + (search-forward "TODO initial next task") + (search-forward "TODO initial last task") + (goto-char (point-min)) + + (search-forward "NEXT Task 0") + (org-todo 'done) + (goto-char (point-min)) + (search-forward "DONE Task 0") + (search-forward "DONE finished task") + (search-forward "NEXT initial next task") + (search-forward "TODO initial last task"))))) + + (describe + "when org-reverse-note-order is nil" + + (before-each (setq org-reverse-note-order nil)) + (after-each (ogt--reset-var 'org-reverse-note-order)) + + (it "as the last NEXT task" + (ogt-capture-and-process-project "project headline") + (ogt-capture-and-process-addition-to-project "Task 0" "project SPC headline TAB RET") + (org-gtd-engage) + (with-current-buffer org-agenda-this-buffer-name + (goto-char (point-min)) + + (expect (ogt--current-buffer-raw-text) :not :to-match "Task 0") + (search-forward "Task 1")))) (it "keeps sanity of TODO states in modified project" (let* ((temporary-file-directory org-gtd-directory) (gtd-file (make-temp-file "foo" nil ".org" (org-file-contents "test/fixtures/gtd-file.org")))) - (ogt--add-single-item "Task 0") - (org-gtd-process-inbox) - (with-simulated-input "[1/3] SPC addtaskhere RET" - (org-gtd--modify-project)) + (org-gtd-core-prepare-buffer (find-file-noselect gtd-file)) + (ogt-capture-and-process-addition-to-project "Task 0" "[1/3] SPC addtaskhere RET") (with-current-buffer (find-file-noselect gtd-file) (search-forward "addtaskhere") (org-narrow-to-subtree) - (expect (ogt--current-buffer-raw-text) :to-match "DONE finished task") - (expect (ogt--current-buffer-raw-text) :to-match "NEXT initial next task") - (expect (ogt--current-buffer-raw-text) :to-match "TODO initial last task") - (expect (ogt--current-buffer-raw-text) :to-match "TODO Task 0") + (search-forward "DONE finished task") + (search-forward "NEXT initial next task") + (search-forward "TODO initial last task") + (search-forward "TODO Task 0") + (goto-char (point-min)) (search-forward "NEXT") (org-entry-put (org-gtd-projects--org-element-pom (org-element-at-point)) "TODO" "DONE") - - (expect (ogt--current-buffer-raw-text) :to-match "DONE finished task") - (expect (ogt--current-buffer-raw-text) :to-match "DONE initial next task") - (expect (ogt--current-buffer-raw-text) :to-match "NEXT initial last task") - (expect (ogt--current-buffer-raw-text) :to-match "TODO Task 0")))) - - - ) + (goto-char (point-min)) + (search-forward "DONE finished task") + (search-forward "DONE initial next task") + (search-forward "NEXT initial last task") + (search-forward "TODO Task 0"))))) diff --git a/test/projects-test.el b/test/projects-test.el index 82448c9..5c0d6a8 100644 --- a/test/projects-test.el +++ b/test/projects-test.el @@ -8,47 +8,103 @@ (describe "Project management" - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem) - (ogt--add-and-process-project "project headline")) - - (after-each (ogt--close-and-delete-files)) - - (describe "marks all undone tasks of a canceled project as canceled" - (it "on a task in the agenda" - (org-gtd-engage) - (with-current-buffer org-agenda-buffer - (goto-char (point-min)) - (search-forward "Task 1") - (org-gtd-agenda-cancel-project) - (org-gtd-archive-completed-items)) - (let ((archived-projects (ogt--archive-string))) - (expect archived-projects :to-match "project headline"))) - - (it "when on the heading" - (setq org-gtd-directory (make-temp-file "org-gtd" t)) - (ogt--add-and-process-project "project headline") - (with-current-buffer (org-gtd--default-file) - (goto-char (point-min)) - (search-forward "project headline") - (org-gtd-cancel-project) - (org-gtd-archive-completed-items) - (basic-save-buffer)) - (let ((archived-projects (ogt--archive-string))) - (expect archived-projects :to-match "project headline")))) - - - (it "safely adds the stats cookie" - (setq org-gtd-process-item-hooks '(org-set-tags-command org-priority)) - (ogt--add-single-item "project headline") + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files) + ;; TODO figure out if this can / should be removed + (remove-hook 'post-command-hook 'org-add-log-note)) + + (describe + "marks all undone tasks of a canceled project as canceled" + (it "on a task in the agenda" + (ogt-capture-and-process-project "project headline") + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (goto-char (point-min)) + (search-forward "Task 1") + (org-gtd-project-cancel-from-agenda) + (org-gtd-archive-completed-items)) + + (let ((archived-projects (ogt--archive-string))) + (expect archived-projects :to-match "project headline"))) + + (it "when on the heading" + (ogt-capture-and-process-project "project tailline") + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "project tailline") + (org-gtd-project-cancel) + (org-gtd-archive-completed-items) + (basic-save-buffer)) + + (let ((archived-projects (ogt--archive-string))) + (expect archived-projects :to-match "project tailline")))) + + (describe + "displaying the guide when the project is poorly shaped" + (it "does it" + (with-simulated-input "SPC" + (org-gtd-projects--show-error)) + (expect (ogt--buffer-string "*Message*") + :to-match "** First Task")))) + +(describe + "Clarifying a project" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs) + (setq org-gtd-clarify-project-templates + '(("prepare a video" . "* think of topic\n* record video\n* edit video")))) + (after-each (ogt--close-and-delete-files) + (setq org-gtd-clarify-project-templates nil) + ;; TODO figure out if this can / should be removed + ;(remove-hook 'post-command-hook 'org-add-log-note) + ) + + (it "allows insertion of a project template" + (ogt-capture-single-item "New project") (org-gtd-process-inbox) - (execute-kbd-macro (kbd "M-> RET")) - (insert ogt--project-text) - (execute-kbd-macro (kbd "C-c c p this_is_a_tag RET A")) - (ogt--save-all-buffers) - (with-current-buffer (org-gtd--default-file) - (goto-char (point-min)) + (with-simulated-input "prepare SPC a SPC video RET" + (org-gtd-clarify-project-insert-template)) + (org-gtd-organize) + (ogt-clarify-as-project) + (org-gtd-engage) + (with-current-buffer org-agenda-buffer (expect (ogt--current-buffer-raw-text) - :to-match "[0/3]") - (search-forward "project headline")))) + :to-match + "think of topic")))) + +;;; TODO uncomment this test +;;; TODO stat cookie should support percent and tally versions + +;; (it "safely adds the stats cookie" +;; (setq org-gtd-organize-hooks '(org-set-tags-command org-priority)) +;; (ogt-capture-single-item "project headline") +;; (org-gtd-process-inbox) +;; (execute-kbd-macro (kbd "M-> RET")) +;; (insert ogt--project-text) +;; (execute-kbd-macro (kbd "C-c c p headline_tag RET A task_1_tag RET B task_2_tag RET C task_3_tag RET A")) +;; (ogt--save-all-buffers) +;; (with-current-buffer (org-gtd--default-file) +;; (goto-char (point-min)) +;; (expect (ogt--current-buffer-raw-text) +;; :to-match "[0/3]") +;; (search-forward "project headline") +;; (expect (member "headline_tag" (org-get-tags))) +;; (expect (org-element-property :priority (org-element-at-point)) +;; :to-equal (org-priority-to-value "A")) +;; (search-forward "Task 1") +;; (expect (member "task_1_tag" (org-get-tags))) +;; (expect (org-element-property :priority (org-element-at-point)) +;; :to-equal (org-priority-to-value "B")) +;; (search-forward "Task 2") +;; (expect (member "task_2_tag" (org-get-tags))) +;; (expect (org-element-property :priority (org-element-at-point)) +;; :to-equal (org-priority-to-value "C")) +;; (search-forward "Task 3") +;; (expect (member "task_3_tag" (org-get-tags))) +;; (expect (org-element-property :priority (org-element-at-point)) +;; :to-equal (org-priority-to-value "A")))) +;; ) diff --git a/test/refiling-test.el b/test/refiling-test.el index 078d5b0..394cb2a 100644 --- a/test/refiling-test.el +++ b/test/refiling-test.el @@ -5,36 +5,43 @@ (require 'buttercup) (require 'with-simulated-input) -(describe "When refiling a project" +(describe + "When refiling a project" - (before-each - (ogt--configure-emacs) - (ogt--prepare-filesystem) - (ogt--add-and-process-project "project headline")) + :var ((inhibit-message t)) - (after-each (ogt--close-and-delete-files)) + (before-each + (ogt--configure-emacs) + (ogt-capture-and-process-project "project headline") + (with-current-buffer (org-gtd--default-file) + (basic-save-buffer))) - (it "skips refiling choice if option is enabled" - (let ((org-gtd-refile-to-any-target t) - (temp-buffer (find-file-noselect (make-temp-file "foo")))) + (after-each (ogt--close-and-delete-files)) - (with-current-buffer temp-buffer - (insert "* foobar") - (org-gtd--refile org-gtd-projects)) + (it "skips refiling choice if option is enabled" + (let ((org-gtd-refile-to-any-target t) + (temp-buffer (get-buffer-create (generate-new-buffer-name "wip")))) - (with-current-buffer (org-gtd--default-file) - (expect (ogt--current-buffer-raw-text) :to-match "foobar")))) + (with-current-buffer temp-buffer + (org-mode) + (insert "* foobar") + (org-gtd--refile org-gtd-projects org-gtd-projects-template)) + + (with-current-buffer (org-gtd--default-file) + (expect (ogt--current-buffer-raw-text) :to-match "foobar")) + + (kill-buffer temp-buffer))) - (describe "finding a refile target" + (describe + "finding a refile target" + (before-each (setq org-gtd-refile-to-any-target nil)) - (it "finds the Project target" - (expect (caar (with-org-gtd-refile - org-gtd-projects - (org-refile-get-targets))) - :to-equal - "Projects")) + (it "finds the Project target" + (let ((targets (caar (with-org-gtd-refile org-gtd-projects + (org-refile-get-targets))))) + (expect targets :to-equal "Projects"))) - (it "finds the Incubate headings in the incubate file" + (it "finds the Incubate headings in the incubate file" (with-current-buffer (org-gtd--default-file) (goto-char (point-max)) (insert "* To Read @@ -46,29 +53,31 @@ :ORG_GTD: Incubated :END:") (save-buffer)) - (with-org-gtd-refile - org-gtd-incubated + (with-org-gtd-refile org-gtd-incubate (let ((ogt-target-names (mapcar 'car (org-refile-get-targets)))) (expect ogt-target-names :to-have-same-items-as '("To Eat" "To Read")))))) - (describe "And I have multiple files as possible targets" + (describe + "And I have multiple files as possible targets" - (it "offers refiling targets" - (let ((new-buffer (create-additional-project-target "more-projects")) - (temp-buffer (find-file "/tmp/test.org"))) + (it "offers refiling targets" + (let ((org-gtd-refile-to-any-target nil) + (new-buffer (create-additional-project-target "more-projects")) + (temp-buffer (get-buffer-create (generate-new-buffer-name "wip")))) (with-current-buffer temp-buffer + (org-mode) (insert "* choose-refile-target") (point-min) (with-simulated-input - "AdditionalHeading RET" - (org-gtd--refile org-gtd-projects))) + "AdditionalHeading RET" + (org-gtd--refile org-gtd-projects org-gtd-projects-template))) (expect (with-current-buffer new-buffer (ogt--current-buffer-raw-text)) :to-match "choose-refile-target") (ogt--clear-file-and-buffer new-buffer) - (ogt--clear-file-and-buffer temp-buffer))))) + (kill-buffer temp-buffer))))) diff --git a/test/reviews-test.el b/test/reviews-test.el new file mode 100644 index 0000000..f698038 --- /dev/null +++ b/test/reviews-test.el @@ -0,0 +1,48 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Reviews" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs) + (add-hook 'org-gtd-organize-hooks #'org-gtd-set-area-of-focus) + (setq org-gtd-areas-of-focus '("Health" "Home" "Career"))) + (after-each (ogt--close-and-delete-files) + (remove-hook 'org-gtd-organize-hooks #'org-gtd-set-area-of-focus) + (setq org-gtd-areas-of-focus nil)) + + (describe + "Areas of focus" + + (it "throws an error if called programmatically with an area not in the list" + (expect + (org-gtd-review-area-of-focus "Playing") + :to-throw + 'org-gtd-invalid-area-of-focus)) + + (it "shows projects, next actions, habits, incubated items in agenda for a specific area of focus" + (let ((task-buffer (ogt--create-org-file-in-org-gtd-dir + "foo" + (org-file-contents + "test/fixtures/areas-of-focus.org")))) + + (org-gtd-review-area-of-focus "Home" "2021-11-20") + + (with-current-buffer org-agenda-buffer + (let ((active-projects "Active projects[[:space:]].*?Fix the roof") + (next-actions "Next actions[[:space:]].*?Clean gutters") + (reminders "Reminders[[:space:]].*?20 November 2021[[:space:]].*?Meet plumber") + (routines "Routines[[:space:]].*?20 November 2021[[:space:]].*?Sweep the") + (incubated-items "Incubated items[[:space:]].*?For later")) + (expect (buffer-name) :to-equal "*Org Agenda: Home*") + (expect (ogt--current-buffer-raw-text) :to-match active-projects) + (expect (ogt--current-buffer-raw-text) :to-match next-actions) + (expect (ogt--current-buffer-raw-text) :to-match reminders) + (expect (ogt--current-buffer-raw-text) :to-match routines) + (expect (ogt--current-buffer-raw-text) :to-match incubated-items))))))) diff --git a/test/single-action-test.el b/test/single-action-test.el new file mode 100644 index 0000000..56d97f5 --- /dev/null +++ b/test/single-action-test.el @@ -0,0 +1,22 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "A single action" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "can be added programmatically" + (org-gtd-single-action-create "Write this test") + (org-gtd-engage) + (with-current-buffer org-agenda-buffer + (expect (ogt--current-buffer-raw-text) + :to-match + "Write this test")))) diff --git a/test/trash-test.el b/test/trash-test.el new file mode 100644 index 0000000..80efca4 --- /dev/null +++ b/test/trash-test.el @@ -0,0 +1,19 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Processing a trash item" + + :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "through the inbox, mark a task as trash" + (ogt-capture-and-process-trash-item "Yowza") + (with-current-buffer (ogt--archive) + (expect (buffer-string) :to-match "Yowza")))) diff --git a/test/upgrades-test.el b/test/upgrades-test.el new file mode 100644 index 0000000..a88d513 --- /dev/null +++ b/test/upgrades-test.el @@ -0,0 +1,171 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(load "test/helpers/utils.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(describe + "Upgrading org-gtd" + +; :var ((inhibit-message t)) + + (before-each (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (describe + "To v3" + + (it "moves calendar items away from using SCHEDULED" + (with-current-buffer (org-gtd--default-file) + (insert """ +* Calendared +:PROPERTIES: +:ORG_GTD: Calendar +:END: +** Twitch Affiliate anniversary +:PROPERTIES: +:LAST_REPEAT: [2023-01-03 Tue 21:59] +:END: +<2023-12-11 Mon +1y> + +** Workout :@workout: +SCHEDULED: <2023-01-04 Wed .+1d> +:PROPERTIES: +:STYLE: habit +:LAST_REPEAT: [2023-01-03 Tue 21:58] +:CATEGORY: Health +:Effort: 30min +:END: + +I think the text goes here + +** record meaningful memories +SCHEDULED: <2023-04-03> + +Do that thing. +""") + (basic-save-buffer)) + (org-gtd-upgrades-calendar-items-to-v3) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Twitch") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-be + nil) + (search-forward "Workout") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-be nil) + (search-forward "memories") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-equal "<2023-04-03>"))) + + (it "moves delegated items away from using SCHEDULED" + (with-current-buffer (org-gtd--default-file) + (insert """ +* Incubated +:PROPERTIES: +:ORG_GTD: Actions +:END: + +** NEXT take a nice nap + +** WAIT record meaningful memories +SCHEDULED: <2023-04-03> +:PROPERTIES: +:DELEGATED_TO: Someone +:END: + +Do that thing. +""") + (basic-save-buffer)) + (org-gtd-upgrades-delegated-items-to-v3) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "nice nap") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-be nil) + (search-forward "memories") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-equal "<2023-04-03>") + )) + + (it "moves incubate items away from using SCHEDULED" + (with-current-buffer (org-gtd--default-file) + (insert """ +* Incubated +:PROPERTIES: +:ORG_GTD: Incubated +:END: + +** take a nice nap + +** record meaningful memories +SCHEDULED: <2023-04-03> + +Do that thing. +""") + (basic-save-buffer)) + (org-gtd-upgrades-incubated-items-to-v3) + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "nice nap") + (expect (org-entry-get (point) org-gtd-timestamp) + :to-be nil) + (expect (org-entry-get (point) "ORG_GTD" t) + :to-equal org-gtd-incubate) + (search-forward "memories") + (expect (org-entry-get (point) "ORG_GTD" t) + :to-equal org-gtd-incubate) + (expect (org-entry-get (point) org-gtd-timestamp) + :to-equal "<2023-04-03>"))) + + + (it "moves habits to their own tree" + (with-current-buffer (org-gtd--default-file) + (insert """ +* Calendared +:PROPERTIES: +:ORG_GTD: Calendar +:END: +** Twitch Affiliate anniversary +:PROPERTIES: +:LAST_REPEAT: [2023-01-03 Tue 21:59] +:END: +<2023-12-11 Mon +1y> + +** Workout :@workout: +SCHEDULED: <2023-01-04 Wed .+1d> +:PROPERTIES: +:STYLE: habit +:LAST_REPEAT: [2023-01-03 Tue 21:58] +:CATEGORY: Health +:Effort: 30min +:END: + +I think the text goes here + +** record meaningful memories +SCHEDULED: <2023-04-03> + +Do that thing. +""") + (basic-save-buffer)) + (org-gtd-upgrades-habits-to-v3) + + (with-current-buffer (org-gtd--default-file) + (goto-char (point-min)) + (search-forward "Workout") + (expect (org-entry-get nil "ORG_GTD" t) + :to-equal org-gtd-habit) + + (goto-char (point-min)) + (search-forward "Twitch Affiliate") + (expect (org-entry-get nil "ORG_GTD" t) + :to-equal org-gtd-calendar) + + (goto-char (point-min)) + (search-forward "record meaningful") + (expect (org-entry-get nil "ORG_GTD" t) + :to-equal org-gtd-calendar))))) diff --git a/test/wip-buffer-test.el b/test/wip-buffer-test.el new file mode 100644 index 0000000..808d5b5 --- /dev/null +++ b/test/wip-buffer-test.el @@ -0,0 +1,47 @@ +;; -*- lexical-binding: t; coding: utf-8 -*- + +(load "test/helpers/setup.el") +(require 'org-gtd) +(require 'buttercup) +(require 'with-simulated-input) + +(defun ogt-manage-active-minor-modes () + "Get a list of which minor modes are enabled in the current buffer. Taken from +https://emacs.stackexchange.com/a/62414/61 +because I need to run this on older emacsen than 28.1 which has +`local-minor-modes'." + (let ($list) + (mapc (lambda ($mode) + (condition-case nil + (if (and (symbolp $mode) (symbol-value $mode)) + (setq $list (cons $mode $list))) + (error nil))) + minor-mode-list) + (sort $list 'string<))) + +(describe + "WIP state for tasks" + + :var ((inhibit-message t)) + + (before-each + (ogt--configure-emacs)) + (after-each (ogt--close-and-delete-files)) + + (it "holds the subtree for the task we want to clarify" + (let ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify"))) + (with-current-buffer source-buffer + (org-gtd-clarify-item)) + + (expect (ogt--buffer-string (car (org-gtd-clarify--get-buffers))) + :to-match + "This is the heading to clarify"))) + + (it "has the org-gtd-processing mode" + (let ((source-buffer (ogt--temp-org-file-buffer "taskfile" "* This is the heading to clarify"))) + (with-current-buffer source-buffer + (org-gtd-clarify-item)) + + (let ((wip-buffer (car (org-gtd-clarify--get-buffers)))) + (with-current-buffer wip-buffer + (expect (ogt-manage-active-minor-modes) :to-contain 'org-gtd-clarify-mode))))))