Org Edna

Table of Contents

Copying

Copyright (C) 2017 Ian Dunn

This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.

Introduction

Extensible Dependencies ’N’ Actions (EDNA) for Org Mode tasks

Edna provides an extensible means of specifying conditions which must be fulfilled before a task can be completed and actions to take once it is.

Org Edna runs when either the BLOCKER or TRIGGER properties are set on a headline, and when it is changing from a TODO state to a DONE state.

For brevity, we use TODO state to indicate any state in org-not-done-keywords, and DONE state to indicate any state in org-done-keywords.

Installation and Setup

Requirements

Emacs 25.1
seq 2.19
org 9.0.5

Right now, the only way to install Edna is by cloning the bazaar repo:

bzr branch https://bzr.savannah.gnu.org/r/org-edna-el/ org-edna
make -C org-edna compile autoloads

After that, add the following to your init file (typically .emacs):

(add-to-list 'load-path "/full/path/to/org-edna/")
(load "/path/to/org-edna/org-edna-autoloads.el")
(org-edna-load)

If you ever want to disable Edna, run org-edna-unload.

Basic Operation

Let’s start with an example: Say you want to do laundry, but once you’ve put your clothes in the washer, you forget about it. Even with a tool like org-notify or appt, Org won’t know when to remind you. If you’ve got them scheduled for an hour after the other, maybe you forgot one time, or ran a little late. Now Org will remind you too early.

Edna can handle this for you like so:

* TODO Put clothes in washer
  SCHEDULED: <2017-04-08 Sat 09:00>
  :PROPERTIES:
  :TRIGGER: next-sibling scheduled!("++1h")
  :END:
* TODO Put clothes in dryer
  :PROPERTIES:
  :TRIGGER: next-sibling scheduled!("++1h")
  :BLOCKER:  previous-sibling
  :END:
* TODO Fold laundry
  :PROPERTIES:
  :TRIGGER: next-sibling scheduled!("++1h")
  :BLOCKER:  previous-sibling
  :END:
* TODO Put clothes away
  :PROPERTIES:
  :TRIGGER: next-sibling scheduled!("++1h")
  :BLOCKER:  previous-sibling
  :END:

After you’ve put your clothes in the washer and mark the task DONE, Edna will schedule the following task for one hour after you set the first headline as done.

Another example might be a checklist that you’ve done so many times that you do part of it on autopilot:

* TODO Address all TODOs in code
* TODO Commit Code to Repository

The last thing anyone wants is to find out that some part of the code on which they’ve been working for days has a surprise waiting for them. Once again, Edna can help:

* TODO Address all TODOs in code
  :PROPERTIES:
  :BLOCKER: file("main.cpp") file("code.cpp") re-search?("TODO")
  :END:
* TODO Commit Code to Repository

Blockers

A blocker indicates conditions which must be met in order for a headline to be marked as DONE. Typically, this will be a list of headlines that must be marked as DONE.

Triggers

A trigger is an action to take when a headline is set to done. For example, scheduling another task, marking another task as TODO, or renaming a file.

Syntax

Edna has its own language for commands, the basic form of which is KEYWORD(ARG1 ARG2 …)

KEYWORD can be any valid lisp symbol, such as key-word, KEY_WORD!, or keyword?.

Each argument can be one of the following:

  • A symbol, such as arg or org-mode
  • A quoted string, such as “hello” or “My name is Edna”
  • A number, such as 0.5, +1e3, or -5
  • A UUID, such as c5e30c76-879a-494d-9281-3a4b559c1a3c

Each argument takes specific datatypes as input, so be sure to read the entry before using it.

The parentheses can be omitted for commands with no arguments.

Basic Features

The most basic features of Edna are finders and actions.

Finders

A finder specifies locations from which to test conditions or perform actions. These locations are referred to as “targets”.

More than one finder may be used. In this case, the targets are merged together, removing any duplicates.

ancestors

Syntax: ancestors

The ancestors finder returns a list of the current headline’s ancestors.

For example:

* TODO Heading 1
** TODO Heading 2
** TODO Heading 3
*** TODO Heading 4
**** TODO Heading 5
     :PROPERTIES:
     :BLOCKER:  ancestors
     :END:

In the above example, “Heading 5” will be blocked until “Heading 1”, “Heading 3”, and “Heading 4” are marked “DONE”, while “Heading 2” is ignored.

chain-find

Syntax: chain-find(OPTION OPTION…)

Identical to the chain argument in org-depend, chain-find selects its single target using the following method:

  1. Creates a list of possible targets
  2. Filters the targets from Step 1
  3. Sorts the targets from Step 2

After this is finished, chain-find selects the first target in the list and returns it.

One option from each of the following three categories may be used; if more than one is specified, the last will be used.

Selection

  • from-top: Select siblings of the current headline, starting at the top
  • from-bottom: As above, but from the bottom
  • from-current: Selects siblings, starting from the headline (wraps)
  • no-wrap: As above, but without wrapping

Filtering

  • todo-only: Select only targets with TODO state set that isn’t a DONE keyword
  • todo-and-done-only: Select all targets with a TODO state set

Sorting

  • priority-up: Sort by priority, highest first
  • priority-down: Same, but lowest first
  • effort-up: Sort by effort, highest first
  • effort-down: Sort by effort, lowest first

children

Syntax: children

The children finder returns a list of the immediate children of the current headline.

In order to get all levels of children of the current headline, use the descendants keyword instead.

descendants

Syntax: descendants

The descendants finder returns a list of all descendants of the current headline.

* TODO Heading 1
   :PROPERTIES:
   :BLOCKER:  descendants
   :END:
** TODO Heading 2
*** TODO Heading 3
**** TODO Heading 4
***** TODO Heading 5

In the above example, “Heading 1” will block until Headings 2, 3, 4, and 5 are DONE.

file

  • Syntax: file(“FILE”)

The file finder finds a single file, specified as a string. The returned target will be the minimum point in the file.

Note that with the default condition, file won’t work. See conditions for how to set a different condition. For example:

* TODO Test
  :PROPERTIES:
  :BLOCKER:  file("~/myfile.org") headings?
  :END:

Here, “Test” will block until myfile.org is clear of headlines.

first-child

  • Syntax: first-child

The first-child finder returns the first child of a headline, if any.

ids

  • Syntax: id(ID1 ID2 …)

The ids finder will search for headlines with given IDs, using org-id. Any number of UUIDs may be specified. For example:

* TODO Test
  :PROPERTIES:
  :BLOCKER:  ids(62209a9a-c63b-45ef-b8a8-12e47a9ceed9 6dbd7921-a25c-4e20-b035-365677e00f30)
  :END:

Here, “Test” will block until the headline with ID 62209a9a-c63b-45ef-b8a8-12e47a9ceed9 and the headline with ID 6dbd7921-a25c-4e20-b035-365677e00f30 are set to “DONE”.

Note that UUIDs need not be quoted; Edna will handle that for you.

match

  • Syntax: match(“MATCH-STRING” SCOPE SKIP)

The match keyword will take any arguments that org-map-entries usually takes. In fact, the arguments to match are passed straight into org-map-entries.

* TODO Test
  :PROPERTIES:
  :BLOCKER:  match("test&mine" agenda)
  :END:

“Test” will block until all entries tagged “test” and “mine” in the agenda files are marked DONE.

See the documentation for org-map-entries for a full explanation of the first argument.

next-sibling

  • Syntax: next-sibling

The next-sibling keyword returns the next sibling of the current heading, if any.

olp

  • Syntax: olp(“FILE” “OLP”)

Finds the heading given by OLP in FILE. Both arguments are strings.

* TODO Test
  :PROPERTIES:
  :BLOCKER:  olp("test.org" "path/to/heading")
  :END:

“Test” will block if the heading “path/to/heading” in “test.org” is not DONE.

org-file

  • Syntax: org-file(“FILE”)

A special form of file, org-file will find FILE in org-directory.

* TODO Test
  :PROPERTIES:
  :BLOCKER:  org-file("test.org")
  :END:

Note that the file still requires an extension.

parent

  • Syntax: parent

Returns the parent of the current headline, if any.

previous-sibling

  • Syntax: previous-sibling

Returns the previous sibling of the current headline on the same level.

rest-of-siblings

  • Syntax: rest-of-siblings

Starting from the headline following the current one, all same-level siblings are returned.

self

  • Syntax: self

Returns the current headline.

siblings

  • Syntax: siblings

Returns all siblings of the source heading as targets.

siblings-wrap

  • Syntax: siblings-wrap

Finds the siblings on the same level as the current headline, wrapping when it reaches the end.

Actions

Once Edna has collected its targets for a trigger, it will perform actions on them.

Actions must always end with ’!’.

Scheduled/Deadline

  • Syntax: scheduled!(OPTIONS)
  • Syntax: deadline!(OPTIONS)

There are several forms that the planning keywords can take:

  • PLANNING(“WKDY[ TIME]”)

    Sets PLANNING to the following weekday WKDY at TIME. If TIME is not specified, only a date will be added to the target.

    WKDY is a weekday or weekday abbreviation (see org-read-date)

    TIME is a time string HH:MM, etc.

  • PLANNING(rm|remove)

    Remove PLANNING from all targets. The argument to this form may be either a string or a symbol.

  • PLANNING(copy|cp)

    Copy PLANNING info verbatim from the current headline to all targets. The argument to this form may be either a string or a symbol.

  • PLANNING(“[+|-][+|-]NTHING”)

    Increment(+) or decrement(-) source (double) or current (single) PLANNING by N THINGs

    N is an integer

    THING is one of y (years), m (months), d (days), h (hours), or M (minutes)

Examples:

scheduled!(“Mon 09:00”) -> Set SCHEDULED to the following Monday at 9:00

TODO State

  • Syntax: todo!(NEW-STATE)

Sets the TODO state of the target headline to NEW-STATE.

NEW-STATE may either be a string or a symbol denoting the new TODO state.

Archive

  • Syntax: archive!

Archives all targets with confirmation.

Confirmation is controlled with org-edna-prompt-for-archive. If this option is nil, Edna will not ask before archiving the target.

Chain Property

  • Syntax: chain!(“PROPERTY”)

Copies PROPERTY from the source entry to all targets.

Clocking

  • Syntax: clock-in!
  • Syntax: clock-out!

Clocks into or out of all targets.

clock-in! has no special handling of targets, so be careful when specifying multiple targets.

Property

  • Syntax: set-property!(“PROPERTY”,“VALUE”)

Sets the property PROPERTY on all targets to VALUE.

  • Syntax: delete-property!(“PROPERTY”)

Deletes the property PROPERTY from all targets.

Priority

  • Syntax: set-priority!(PRIORITY)

Sets the priority of all targets to PRIORITY. PRIORITY is processed as follows:

  • If PRIORITY is a string, the first character is used as the priority
  • Any other value is passed into org-priority verbatim, so it can be ’up, ’down, or an integer

Tag

  • Syntax: tag!(“TAG-SPEC”)

Tags all targets with TAG-SPEC, which is any valid tag specification, e.g. tag1:tag2

Effort

  • Syntax: set-effort!(VALUE)

Sets the effort of all targets according to VALUE:

  • If VALUE is a string, then the effort is set to VALUE
  • If VALUE is an integer, then set the value to the VALUE’th allowed effort property
  • If VALUE is the symbol ’increment, increment effort

Advanced Features

Conditions

Edna gives you he option to specify blocking conditions. Each condition is checked for each of the specified targets; if one of the conditions returns true for that target, then that headline is blocked.

done

  • Syntax: done?

Blocks the current headline if any target is DONE.

headings

  • Syntax: headings?

Blocks the current headline if any target belongs to a file that has an Org heading.

org-file("refile.org") headings?

The above example blocks if refile.org has any headings.

todo-state

  • Syntax: todo-state?(STATE)

Blocks if any target has a headline with TODO state set to STATE.

STATE may be a string or a symbol.

variable-set

  • Syntax: variable-set?(VARIABLE,VALUE)

Blocks the current headline if VARIABLE is set to VALUE.

VARIABLE should be a symbol, and VALUE is any valid lisp expression

self variable-set?(test-variable,12)

has-property

  • Syntax: has-property?(“PROPERTY”,“VALUE”)

Tests each target for the property PROPERTY, and blocks if it’s set to VALUE.

Negating Conditions

Any condition can be negated using ’!’.

match("test") !has-property?("PROP","1")

The above example will cause the current headline to block if any headline tagged “test” does not have the property PROP set to “1”.

Consideration

Special keyword that’s only valid for blockers.

This keyword can allow specifying only a portion of tasks to consider:

  1. consider(PERCENT)
  2. consider(NUMBER)
  3. consider(all) (Default)

(1) tells the blocker to only consider some portion of the targets. If at least PERCENT of them are in a DONE state, allow the task to be set to DONE. PERCENT must be a decimal.

(2) tells the blocker to only consider NUMBER of the targets.

(3) tells the blocker to consider all following targets.

A consideration must be specified before the targets to which it applies:

consider(0.5) siblings consider(all) match("find_me")

The above code will allow task completion if at least half the siblings are complete, and all tasks tagged “find_me” are complete.

consider(1) ids(ID1 ID2 ID3) consider(2) ids(ID3 ID4 ID5 ID6)

The above code will allow task completion if at least one of ID1, ID2, and ID3 are complete, and at least two of ID3, ID4, ID5, and ID6 are complete.

If no consideration is given, ALL is assumed.

Extending Edna

Extending Edna is (relatively) simple.

During operation, Edna searches for functions of the form org-edna-TYPE/KEYWORD.

Naming Conventions

In order to distinguish between actions, finders, and conditions, we add ’?’ to conditions and ’!’ to actions. This is taken from the practice in Guile and Scheme to suffix destructive functions with ’!’ and predicates with ’?’.

Thus, one can have an action that files a target, and a finder that finds a file.

Finders

Finders have the form org-edna-finder/KEYWORD, like so:

(defun org-edna-finder/test-finder ()
  (list (point-marker)))

All finders must return a list of markers, one for each target found, or nil if no targets were found.

Actions

Actions have the form org-edna-action/KEYWORD!:

(defun org-edna-action/test-action! (last-entry arg1 arg2)
  )

Each action has at least one argument: last-entry. This is a marker for the current entry (not to be confused with the current target).

The rest of the arguments are the arguments specified in the form.

Conditions

(defun org-edna-condition/test-cond? (neg))

All conditions have at least one argument, “NEG”. If NEG is non-nil, the condition should be negated.

Most conditions have the following form:

(defun org-edna-condition/test-condition? (neg)
  (let ((condition (my-test-for-condition)))
    (when (org-xor condition neg)
      (string-for-blocking-entry-here))))

For conditions, we return true if condition is true and neg is false, or if condition is false and neg is true:

cond neg res
t t f
t f t
f t t
f f f

This is an XOR table, so we pass CONDITION and NEG into org-xor to get our result.

A condition must return a string if the current entry should be blocked.

Contributing

We are all happy for any help you may provide.

First, check out the source code on Savannah: https://savannah.nongnu.org/projects/org-edna-el/

bzr branch https://bzr.savannah.gnu.org/r/org-edna-el/ org-edna

You’ll also want a copy of the most recent Org Mode source:

git clone git://orgmode.org/org-mode.git

Bugs

There are two ways to submit bug reports:

  1. Using the bug tracker at Savannah
  2. Sending an email using org-edna-submit-bug-report

When submitting a bug report, be sure to include the Edna form that caused the bug, with as much context as possible.

Development

Submitting patches along with a bug report is the easiest way.

There are a few rules to follow:

  • Verify that any new Edna keywords follow the appropriate naming conventions
  • Any new keywords should be documented
  • Run ’make check’ to verify that your mods don’t break anything
  • Avoid additional or altered dependencies if at all possible
    • Exception: New versions of Org mode are allowed

Email: dunni@gnu.org

Validate