EMACS-DOCUMENT

=============>随便,谢谢

Org-mode任务依赖的高级应用

2018-02-02更新:org-edna 的链接

本博客的目标受众是 Emacs org-mode 高级用户以及那些对如何使用这些工具完成非琐碎的待办事务管理感到好奇的人。

我们通过第三方贡献的 package org-depend.el 来定义待办任务之间的依赖关系. 该package有一组很好的特性. 不过我的工作流只用到了其中的部分功能。 因此,本文并对 org-depend 进行详细说明。但是给出使用和改进它的一些想法。

使用yasnippet 和 yankpad 进行高级代码片段管理

对于一次性工作流,我手动创建任务并定义依赖项。然而,当我不得不多次做某事时,我倾向于使用snippet software通过快捷键的方式插入标题和任务。我写了一个关于文本片段工具的德语博客

比如我使用 ccn TAB 来插入静态的信用卡号. 通过 dd TAB 静态地插入ISO格式的当前日期,比如 2016-12-17. 下面我还会描述一个复杂的工作流,需要用到一个代码片段工具,以此提供复杂的、交互式的代码片段。

yasnippet 支持从简单到高级的Emacs片段管理了应用. 有了 yasnippet, 你可以根据major-mode(比如org-mode)管理目录中不同文件中的代码片段。 扩展代码片段是通过键入代码片段定义的缩写(例如 dd ),然后接着(通常是)按 TAB 键来完成的。

多年来我一直使用 yasnippet,知道我发现了 yankpad,它是对 yasnippet 的一个封装。与yasnippet相比,yankpad通过Org-mode文件中的不同标题(而不是目录下的不同文件)来管理代码片段 由于我的代码片段多是用于Org-mode中的,所以我很高兴地接受了yankpad,并将所有的org-mode代码片段都迁移到了它上面。这使得维护更加容易,因为我在编写代码片段时能应用完整的org-mode功能。

有了这样一个强力的snippet系统,我只需要定义一次复杂的工作流,而无需在创建工作流实例时处理依赖定义。 因此,对于我日常使用的工作流来说,将复杂性放到代码片段模板中,可以立即让我从中受益。

应用示例: 与朋友组织一个歌舞晚会

我喜欢和朋友一起去看歌舞表演。为了在不忘记重要事情的情况下尽量减少组织工作,我维护了一个简单的工作流程:

  1. 活动基本信息:艺术家,日期,简短描述,…
  2. 定票
  3. 给我的朋友发封邮件,问问谁会参加
  4. 记下每一个参加的人
  5. 在活动开始前不久发一封提醒邮件,这样没人会忘记
  6. 为活动之夜本身创建一个org-mode事件

使用高级的交互式代码片段

假设,我在报纸上无意中看到,我最喜欢的一位艺术家要来。

我切换到用来存储这类事件的org-mode文件。我通过快捷键插入一个yankpad代码片段,然后选择有关歌舞剧的代码片段。 Emacs会询问我活动的日期(使用日期选择器)、艺术家的名字、歌舞表演项目的名称、地点(从预先定义的场地列表中选择)、我将预订多少座位、第一封和第二封电子邮件的日期。

我的yankpad代码片段如下所示。我还定义了一些简单的封装函数来询问和插入内容 my-capture-prompt, my-capture-selection, 以及 my-capture-insert ,这些函数可以在我的Emacs配置中找到。

\** Cabaret `(my-capture-prompt "date of event" 'my-event-date)`: `(my-capture-prompt "artist" 'my-artist)`
:PROPERTIES:
:ID: `(my-capture-insert 'my-event-date)`-cabaret
:END:

- Title: `(my-capture-prompt "title" 'my-title)`
- `(my-capture-prompt "Num of seats" 'my-num-seats)` seats reserved:
  - 2: My girlfriend and I
  - 2:
  - 2:
  - 2:

\*** WAITING Make reservation for `(my-capture-insert 'my-num-seats)` seats
:PROPERTIES:
:ID: `(my-capture-insert 'my-event-date)`-reservation
:TRIGGER: `(my-capture-insert 'my-event-date)`-offer-seats(TODO) `(my-capture-insert 'my-event-date)`-reminder-email(TODO)
:END:

\*** Email: offer `(my-capture-insert 'my-num-seats)`-2 seats
SCHEDULED: <`(my-capture-prompt "date 1st email" 'my-email-date)`>
:PROPERTIES:
:ID: `(my-capture-insert 'my-event-date)`-offer-seats
:END:

Email template:
#+BEGIN_QUOTE
Cabaret: `(my-capture-insert 'my-artist)` on `(my-capture-insert 'my-event-date)`

Hi friends!

Who:    `(my-capture-insert 'my-artist)`
What:   "`(my-capture-insert 'my-title)`"
When:   `(my-capture-insert 'my-event-date)` 19:15
Where:  `(my-capture-selection '("Theatercafé" "Orpheum") 'my-location)`

First come, first served. We've got `(my-capture-insert 'my-num-seats)` seats.

Karl
#+END_QUOTE

\*** Send reminder email
SCHEDULED: <`(my-capture-prompt "date reminder" 'my-reminder-date)`>
:PROPERTIES:
:BLOCKER: `(my-capture-insert 'my-event-date)`-offer-seats
:ID: `(my-capture-insert 'my-event-date)`-reminder-email
:END:

\*** `(my-capture-insert 'my-artist)`: "`(my-capture-insert 'my-title)`" (`(my-capture-insert 'my-location)`)
:PROPERTIES:
:ID: `(my-capture-insert 'my-event-date)`-cabaret-event
:END:

<`(my-capture-insert 'my-event-date)` 20:00-23:30>

你可以看到,通过使用 my-capture-promtmy-capture-insert 函数,我们可以很容易重用事件日期等信息。

应用该代码段并创建一个实例将得到如下结果:

** Cabaret 2017-01-24: Thomas Maurer
:PROPERTIES:
:ID: 2017-01-24-cabaret
:END:

- Title: Der Tolerator
- 8 seats reserved:
  - 2: My girlfriend and I
  - 2:
  - 2:
  - 2:

*** WAITING Make reservation for 8 seats
:PROPERTIES:
:ID: 2017-01-24-reservation
:TRIGGER: 2017-01-24-offer-seats(TODO) 2017-01-24-reminder-email(TODO)
:END:

*** Email: offer 8-2 seats
SCHEDULED: <2017-01-05>
:PROPERTIES:
:ID: 2017-01-24-offer-seats
:END:

Email template:
#+BEGIN_QUOTE
Cabaret: Thomas Maurer on 2017-01-24

Hi friends!

Who:    Thomas Maurer
What:   "Der Tolerator"
When:   2017-01-24 19:15
Where:  Theatercafé

First come, first served. We've got 8 seats.

Karl
#+END_QUOTE

*** Send reminder email
SCHEDULED: <2017-01-21>
:PROPERTIES:
:BLOCKER: 2017-01-24-offer-seats
:ID: 2017-01-24-reminder-email
:END:

*** Thomas Maurer: "Der Tolerator" (Theatercafé)
:PROPERTIES:
:ID: 2017-01-24-cabaret-event
:END:

<2017-01-24 20:00-23:30>

注意,对于不同日期的多个歌舞剧事件,其id是惟一的,因为事件日期是id的一部分,并且所有依赖项都是预先定义好的。

一旦订好票了并将其任务标记为done后,两个发送邮件的任务将通过 :TRIGGER: 变成“TODO”状态。

更新于2017-11-23:状态关键字的传播在某些情况下不能工作

为了演示先决条件被阻塞的情况,我为提醒电子邮件任务添加了 :BLOCKER: 依赖,在这个特定的示例中,它有点多余. 标记了 :BLOCKER: 的标题与普通标题有一个细微的区别:只要阻塞ID没有标记为完成(或取消), :BLOCKER: 任务就不会出现在我的日程中。 这个特性非常棒,因为只要不满足先决条件,我就看不到已定义和计划好的任务。因此,我总是在工作流程中定义 :TRIGGER::BLOCKER: 依赖项,以使我的日程不会被现在无法处理的代办任务搞得一团糟。

定义一个复杂的代码片段需要时间和精力。不过只要您为工作流定义好了复杂的代码片段,就可以轻松地多次设置工作流实例,这是它的优势。

高级工作流

歌舞剧的例子比较简单,只是为了演示基本的思想。我使用的项目模板,eBay购物工作流,Scrum故事管理,甚至是整个学期的整个课程管理,包括考试准备和学生评分则要复杂的多,这些都是由几十个标题组成的。

chain-find-next(KEYWORD[,OPTIONS]) helps you finding the "next" heading. 除了在代码片段中使用的 :TRIGGER::BLOCKER: 依赖之外, org-depend.el 还提供了其他功能。 通过 chain-sibling(KEYWORD), 可以让当前标题被标记为完成时,下一个标题变成 KEYWORD 状态。 还有 chain-siblings-scheduled, 它将在 SCHEDULED 日期切换下一个标题的状态。 下一个标题由 chain-find-next(KEYWORD[,OPTIONS]) 来决定。

虽然这些是很好的特性,但我不使用它们,因为我需要更精细的特性,下面将会讲到。

改进空间

I wrote down some possible improvements that would ease my personal digital life. 由于我是 org-depend.el 的高级用户,再加上 Carsten 在训一些改进 =org-depend.el=的意见,我写下了一些可能会改善我个人数字生活的改进意见。

其中一些问题可能只需要几行Elisp代码就可以解决了。不幸的是,我自己Elisp的编码能力很差,因此无法随心随意地扩展Emacs。

改进:ID选择器

首先,我希望在定义 :TRIGGER::BLOCKER: 依赖时能使用ID选择器。

This should work like this: after setting up the task in headings and giving them IDs, I'd like to invoke a "I want to define a dependency"-command. It first asks me what property I want to set: :TRIGGER: or :BLOCKER:. 它的工作方式应该是这样的:在标题中设置好任务并为其id之后,调用一个“我想定义一个依赖项”的命令,它首先会询问我要设置什么属性: :TRIGGER: 还是 :BLOCKER:

然后选择在相同的子层次结构中找到的所有ID(或者更进一步,所有文件中找到的ID?)

然后为 :TRIGGER: 依赖(如果适用)设置好 KEYWORD 后,相应的属性将添加到当前标题上。

这将极大地改进创建依赖项定义的过程,并在第一时间防止输入错误。

改进:根据标题和日期生成id

到目前为止,我手动定义了 :ID: 属性。有些设置可以为新标题随机生成id。但我不喜欢随机ID,因为我想在看到ID时能有关于标题内容的提示

Usually, my IDs start with the current ISO day to enforce uniqueness and look like this: 通常,我的id以ISO日期开头以保证唯一性,如下所示:

Title Manual ID
Update notebook 2016-12-18-update-notebook
Schedule a meeting with Bob 2016-12-18-schedule-meeting-bob
Add additional URLs to lecture notes 2016-12-18-add-URLs-to-lecture

如果有一个命令能根据当前标题自动生成相应的ID属性,不是很好吗?我想这并不难做到:

Title Auto-generated ID
Update notebook 2016-12-18-Update-notebook
Schedule a meeting with Bob 2016-12-18-Schedule-a-meeting-with-Bob
Add additional URLs to lecture notes 2016-12-18-Add-additional-URLs-to-lecture-notes

改进:创建工作流元素的助手

本建议来至于 Christophe Schockaert,他在 mailinglist中写到: 为什么没有有一个可以同时做多件事情的助手呢?

[...] Besides that, I wonder if/how we could automate the following course of actions:

  • let have point on an entry
  • create a new "TODO-like" entry as a link to that entry
  • assign an ID to both entries: lets say "ID-original" and "ID-duplicate"
  • in the new entry: define a BLOCKER property set on "ID-original"
  • in the original entry: define a TRIGGER property set as ID-duplicate(DONE)

At first sight:

  • the new entry could be created besides the original or in a file where it is ready to refile
  • the TODO state in the new entry could be set with a default, I think it is so easy to switch afterwards with Org keystrokes
  • the triggered state might better be a parameter (possibly a customized default as "TODO"): otherwise, it would be necessary to go inside the drawer to change it

Currently, I am doing all this manually, quite often. [...]

我从中复制了这些内容: 这是一组非常常见的操作,它们经常在一起完成。 但是,我个人希望除了这个助理之外,还有上面提到的功能。

改进: TRIGGER 与 SCHEDULED 设置整合

I love the :TRIGGER: property because I can mark headings as open tasks only if they can be done now. Only headings which are ready to be looked at do have the TODO keyword. 我喜欢=:TRIGGER:=属性,因为我可以将标题标记为打开的任务,除非它们现在就可以完成。只有可以查看的标题才有=TODO=关键字。

One limitation of org-depend.el is that I am only to move forward scheduled dates to siblings and I am not able to define a different scheduled date. =org-depend的一个限制。el=是我只能将计划日期向前移动到兄弟姐妹,我不能定义一个不同的计划日期。

Assume following syntax: 假设如下语法:

** TODO Asking the client about the project
 :PROPERTIES:
 :TRIGGER: 2016-12-18-send-offer(TODO,2016-12-23)
 :END:

** Send offer to client
 :PROPERTIES:
 :ID: 2016-12-18-send-offer
 :END:    

我扩展了trigger属性的选项,以便让keyword参数支持ISO日期。

我希望在完成第一个任务时,ID为 2016-12-18-send-offer 的标题不仅关键字变为 TODO ,而且也被安排在2016-12-23那一天。

请注意, send-offer 标题不需要与 ask-client 标题在同一个子层次中. Therefore, sibling-operations 并不能完成所有的功能.

除此之外,我希望能够定义相对的日程日期,如日期提示手册中那样:

2016-12-18-send-offer(TODO,.) the day when marking the asking-task as done
2016-12-18-send-offer(TODO,+3d) 3 days after the scheduled date of the asking-task
2016-12-18-send-offer(TODO,.+3d) 3 days from the day when marking the asking-task as done
2016-12-18-send-offer(TODO,mon) nearest Monday from the day when marking the asking-task as done
2016-12-18-send-offer(TODO,+2tue) second Tuesday from the day when marking the asking-task as done

改进:取消任务的同时也会取消它们的依赖关系

如果有一个通用设置(或属性?)让我处理取消的任务和完成的任务不是很好吗?

想象一下上面的例子。当我取消了ask-client任务时,send an offer真的有意义吗?许多人会向我一样也想取消所有后续的工作流任务。

结论

虽然大多数人不需要高级工作流管理,比如任务之间的依赖关系,但我确实喜欢这个org-mode特性。 这也是我为什么开始使用org-mode的原因。我喜欢让日程表只显示“现在”可以完成的任务和已经完成的任务。

因此,即使您感受不到使用代码片段/模板系统来定义工作流的急迫之处,您依然可能会喜欢这篇文章。也许您也会开始定义简单的工作流。

I'd love to read your comments on snippets, workflows, dependencies and such: write me an email or commend via Disqus (see below). 欢迎对代码片段、工作流程、依赖关系等内容做出评论:给我发电子邮件或通过Disqus都行(见下面)。

org-edna

有人建议我用 org-edna 代替 org-depend.它给我的第一印象是跟 org-depend 很像但又有所不同。它更复杂,并且允许对依赖项进行更高级的定义。我对org-edna很有兴趣,我将在以后写一些关于它的内容。