文本化的任务收集
「收集」是 GTD 流程中的第一步,面对头脑中随时一闪而过的各种想法,需要能够将其准确而快速地收集到 Inbox 中。
针对这种需求,主流任务管理 app 大多都提供一种「快速添加」的功能,即在界面上提供一个按钮(如 OmniFocus 中的「Save+」),当用户输入完一条任务信息并点击该按钮时,应用不会回到主界面,而是保存该条任务并清空输入框,让用户可以直接继续输入。
很明显,这种添加任务的方式只是一种妥协方案,所谓的「快速添加」并没有那么快捷。用户不仅需要手动打开 app、点击添加按钮,而且如果想修改截止日期、备注等具体参数,仍然需要多次点击。
那么,是否存在这样一种方式,能够一步到位地批量收集任务并定义其具体参数呢?
事实上,这样的方法是存在的,那就是用文本来描述任务。
来看下面这段语句:
- 任务一 @due(+1d 10p) - 任务二 @defer(tue)
即便不加说明,读者应该也能很容易猜出它的含义。这段语句描述了两个任务 任务一
和 任务二
,其中 任务一
将截止于明天(+1d)
的晚上 10 点(10p)
,而 任务二
推迟至周二(tue)
开始。
这种描述任务的方式来源于 macOS 上一款基于纯文本的任务管理工具—— Taskpaper。
显然,这种基于纯文本的任务描述方法天然地适合批量输入任务,也免去了在各个输入框中来回设置日期、上下文等参数的麻烦——手不离键盘就能完成全部输入过程。和 Omnifocus 原有的添加多任务的方式相比,Taskpaper 语法的优势显得尤为明显。
在 OmniFocus 在 2.14 版后,我们就可以用 Taskpaper 的语法,来批量地向 Omnifocus 来添加任务了。操作过程中,只要通过如下 URL Scheme:
omnifocus:///paste?content=内容
将符合 Taskpaper 格式的文本传送到 OmniFocus,后者即可自动识别其中包含的任务和属性。OmniFocus 目前支持的 Taskpaper 语法如下表所示:
不过,用什么工具承担撰写 Taskpaper 语句并发送给 OmniFocus 的职能呢?理论上,只要是能接受输入并运行 URL Scheme 的 app,如常用的 Workflow、Launch Center Pro 等,都可以做到。不过,本文还是首推 Drafts,这不仅是因为其简洁的界面和宽裕的输入空间十分适合任务收集的情境,更是因为其内建的快捷键和脚本支持能进一步提高输入的效率。基于此,下面就以 Drafts 为例演示如何建立和优化这一工作流。
首先建立最基础的动作,将 Drafts 和 OmniFocus 连接起来。我们在 Drafts 中新建一个动作,其中包含一个 URL 步骤,内容为:
omnifocus:///paste?content=[[draft]]&x-success=drafts4://
并将该动作的「after success」属性改为「Trash」。(你可以点击这里直接导入该动作。)
接着,我们在 Drafts 的工具栏中增加一个指向上述动作的快捷按钮。(你可以点击这里直接导入该按钮。)另外,我还为该按钮设置了一个快捷键(Ctrl-Option-O,与 Mac 版 OmniFocus 的 Quick Entry 保持一致),便于使用 iPad 时直接在键盘上完成全过程。
这样,只要按照 Taskpaper 语法输入需要批量添加的任务,然后点击键盘上方的按钮/按下 Ctrl-Option-O
,就能将这些任务批量添加到 OmniFocus 的 Inbox 中、并自动返回到 Drafts,该条 draft 会在动作执行完毕后被自动删除。
此外,为了加快标签的输入,你还可以将下面这些常用标签的快捷键导入 Drafts 中:
它们的效果是点击后在当前位置插入对应标签,并将光标移动到括号之间;如果选中一段文本再点击该按钮,则可以直接将选中文本包括在标签之内。
这样一来,我们就实现了用 Taskpaper 语法批量收集任务到 OmniFocus 的流程。下面的动画演示了在 iPad 上使用快捷键操作的过程:
需要注意的是,目前 OmniFocus 尚未支持指定任务所在的文件夹或项目。好在,得益于 Drafts 强大的脚本功能,我们可以「自力更生」,间接实现指定文件夹/项目的功能;事实上,上文中提供的动作已经增加了对 @project
和 @folder
两种标签的支持。只要在输入时添加 @project(项目名)
或 @folder(文件夹名)
之一,再运行该动作,即可实现发送到指定项目或文件夹的效果。
例如,下面的输入:
- 任务一 @project(项目一) - 任务二
会在名为「项目一」的项目中新建两个任务「任务一」和「任务二」。
需要注意的是:
- 输入的项目名或文件夹名无须完全正确,因为 OmniFocus 对输入有一定的模糊识别能力;
- 由于实现机制本身的限制,每次输入中只能指定一个项目或文件夹,而不能单独指定。如果输入中出现多个
@project
或@folder
标签,所有输入都会被发送到第一个标签指定的项目或文件夹。但反过来说,要给输入的任务/项目指定目的地,只要写一次@project
或@folder
即可,无须每条任务指定一次。
你可以导入这两个标签的快捷输入按钮:
来看一下最终的实现效果:
细节探究
经过上文的配置,我们已经实现了借助 Taskpaper 语法,用纯文本的形式批量快速收集任务到 OmniFocus。那么,这些功能是怎样实现的?如何修改这些功能使其更加符合自己的偏好?为此,下文就来具体说明上面导入的动作和按钮是如何通过 Drafts 内建的 JavaScript 和正则表达式支持实现其功能的。
需要说明的是,Drafts 支持全部的基础 JavaScript 语法及正则表达式,且有较为详尽的官方文档。因此,下文目的并不在于详细介绍语法或翻译文档,而重在对于代码中的一些关键思路和所用到的 Drafts 独有的函数/属性进行介绍。
1. 快捷输入标签的实现机制
Drafts 中的快捷按钮功能相当多样,不仅可以输入特定字符、执行某个动作,还可以运行一段 JavaScript 脚本。以上文中提供的用于输入 @context
标签的按钮为例,点击该按钮执行的代码是:
var sel = getSelectedText(); var selRange = getSelectedRange(); if (!sel || sel.length == 0) { setSelectedText("@context()"); setSelectedRange(selRange[0]+9,0); } else { setSelectedText("@context("+sel+")"); setSelectedRange(selRange[0]+selRange[1]+10,0); }
这段脚本主要使用了与文本选择有关的几个函数:
getSelectedText()
获得的是当前选中的文本getSelectedRange()
将返回两个值,分别是当前光标所在位置和选中文本的长度- 在上述代码中,这两个值被一起传递给了
selRange
- 因此后面用
selRange[0]
和selRange[1]
。
代码的第三行对选中的文本长度进行了判断,如果长度为 0,就直接插入「@context()」这段文本。之后的 setSelectedRange()
函数用于选中文本,它需要输入两个值:光标的起始位置和选中文本的长度,当后一个值为 0 时,就相当于将光标置于前一个值指定的位置。我们希望将光标放在@context
后的括号中间,更精确地说是,放在从 @
算起第 9 个字符的位置,这就是这些代码里总会出现数字 9 的原因的来源。以此类推,如果我们需要输入 @due()
并移动光标,则应相应改为 (selRange[0]+5,0)
。
如果选中文本的长度不为 0,代表我们希望将一段已经输入的文本转化为标签,因此,我们用加号 (+)
将@context
、一个左括号、选中的内容和一个右括号依次连接起来,交给 setSelectedText()
函数。另外,在这种情况下,我们希望光标被放在括号之外,因此交给 setSelectedRange()
的光标起始位置应该是原来的后推@context()
的长度(10 个字符)、再加上选中文本的长度(selRange[1]
),故完整的表达就是 (selRange[0]+selRange[1]+10,0)
。类似地,如果这里是要将选中文本转化为 @due()
标签,则应改为 (selRange[0]+selRange[1]+6,0)
。
2. 指定文件夹或项目的实现机制
如上所述,OmniFocus 没有提供对 @project
或 @folder
标签的原生支持。但是,在其 URL Scheme 中,paste
动作可以附加一个 target
参数,用于指定发送到的具体位置。例如:
omnifocus:///paste?target=/task/general
会将当前的剪贴板内容创建为 General
项目下的任务。[^1]
而下列 URL:
omnifocus:///paste?target=/folder/personal
会将当前的剪贴板内容创建为 Personal
文件夹下的项目。
这就为我们自行创建对 @project
或 @folder
标签的支持提供了可能:只要用正则表达式提取 draft 中出现的相应标签内容,将其填入上述 URL Scheme 并执行即可。事实上,上文提供的动作就用下列 JavaScript 脚本对输入进行了处理:
var text = draft.content; var folderTag = text.match(/@folder\(.+?\)/g); if (folderTag != null) { var target = '/folder/' + folderTag[0].slice(8,-1); } else { var projectTag = text.match(/@project\(.+?\)/g); if (projectTag != null) { var target = '/task/' + projectTag[0].slice(9,-1); } else { var target = 'inbox'; } } draft.defineTag('target',target);
该段代码用到了 Drafts 中特有的 draft 对象及其属性和函数。在 Drafts 中,draft
是一个预先定义的对象,指当前这条 draft。该对象有多种属性:
content
:文本内容createdDate
:创建日期archived
:是否被归档,布尔值uuid
:该条 draft 的唯一代码
上述代码的第一行即将 text
变量定义为当前 draft 的内容(draft.content
)。
接着,代码用正则表达式 /@folder\(.+?\)/g
查找文本中的 @folder
标签,其中 .+
匹配的是不限数量的、换行符以外的任意字符。而将表达式夹在 //g
之间则代表搜索所有的结果。匹配结果被传给了 folderTag
。然后,将第一个匹配到的结果(folderTag[0]
)进行切割,去掉开头的「@folder(」(8 个字符)和结尾的「)」(1 个字符),只留标签括号中的部分(slice(8,-1)
),拼成 /folder/文件夹名
的形式,赋给 target
变量。
如果没有搜索到 @folder
标签,代码将继续搜索 @project
标签,方法类似。如果仍然没有搜索到,说明没有指定项目或文件夹,则将 inbox
赋给 target
变量,即发送到 Inbox。最后,将 target
变量的值定义为一个 Drafts 标签[^2] [[target]]
,以便在之后的步骤中引用。事实上,它将在下一步中,通过 URL Scheme:
omnifocus:///paste?content=[[draft]]&target=[[target]]&x-success=drafts4://
发送给 OmniFocus。
3. 语法细则
OmniFocus 支持大部分的 Taskpaper 语法,但也存在一些变通,下文列举的是两者的主要差异。
关于短横线
在 Taskpaper 中,任务必须以一个短横线(-
)开头,但 OmniFocus 无此要求。因此,单独添加任务时,「- 任务一」和「任务一」这两种写法都可以。
关于备注
在 Taskpaper 语法中,紧跟一条任务之后、不以短横线开头的一行,被认为是该条任务的备注(Notes),例如:
- 任务一备注内容
OmniFocus 同样能识别该语法。在这种场合,任务前的横线不能被省略。例如,不能写成:
任务一备注内容
否则,备注将被识别为另一条任务。
关于子任务
在 Taskpaper 语法中,任务间的隶属关系通过缩进来体现,你可以通过输入一个制表符(Tab 键)将一个任务标记为其上一行的子任务,例如:
- 任务 - 子任务一 - 子任务二
这种情况下,任务名前的横线可以省略。而且你还可以通过 @parallel 标签指定子任务是平行的(@parallel(1)
)还是顺序的(@parallel(0)
),如不指定,默认为平行。
最后,根据 Taskpaper 原本的规则,可以用项目名:
的格式来新建一个项目。但 OmniFocus 并未接受这一设定。因此,即使输入:
项目一: - 任务一 - 任务二
也不能创建一个名为 Task 1
的项目。对于 OmniFocus,该输入与上个例子是等价的。如果想将任务发送到一个既有的项目,你应该如前文所述,使用我们自行定义的 @project
标签。
[^1]: 注意,target
参数的值须经 URL 编码。但为易读性起见,本段中的 URL 没有做此处理。实际执行中,该 URL 会被 Drafts 自动编码为 omnifocus:///paste?target=%2Ftask%2Fgeneral
。
[^2]: 标签(Tags)是 Drafts 另一项重要功能,类似于 Workflow 中的变量。Drafts 提供了一些默认标签,但用户也可以自行定义新的标签。与步骤内部执行的 JavaScript 脚本中的变量相比,标签的优势在于可以在步骤之间传递数据。