拿Emacs对接我的cuckoo

cuckoo是一个我自己开发的类似待办事项的工具,运行在我本地的电脑上。它有如下两个接口:

  1. 传入一个UNIX Epoch时间戳创建提醒
  2. 传入一个标题以及提醒的ID来创建任务

这样一来,便能在设定的时刻调用alerter在屏幕右上角弹出提醒。

我喜欢用Emacsorg-mode来安排任务,但可惜的是,org-mode没有定点提醒的功能(如果有的话希望来个人打我的脸XD)。开发了cuckoo后,忽然灵机一动——何不给Emacs添砖加瓦,让它可以把org-mode中的条目内容(所谓的heading)当做任务丢给cuckoo,以此来实现定点提醒呢。感觉是个好主意,马上着手写这么些Elisp函数。

PS:读者朋友们就不用执着于我的cuckoo究竟是怎样的接口定义了。

为了实现所需要的功能,让我从结果反过来推导一番。首先,需要提炼一个TODO条目的标题和时间戳(用来创建提醒获取ID),才能调用cuckoo的接口。标题就是org-mode中一个TODO条目的heading text,在Emacs中用下面的代码获取

1
(nth 4 (org-heading-components))

org-headline-components在光标位于TODO条目上的时候,会返回许多信息(参见下图)

其中下标为4的component就是我所需要的内容。

接着便是要获取一个提醒的ID。ID当然是从cuckoo的接口中返回的,这就需要能够解析JSON格式的文本。在Emacs中解析JSON序列化后的文本可以用json这个库,示例代码如下

1
2
(let ((s "{\"remind\":{\"create_at\":\"2019-01-11T14:53:59.000Z\",\"duration\":null,\"id\":41,\"restricted_hours\":null,\"timestamp\":1547216100,\"update_at\":\"2019-01-11T14:53:59.000Z\"}}"))
(cdr (assoc 'id (cdr (car (json-read-from-string s))))))

既然知道如何解析(同时还知道如何提取解析后的内容),那么接下来便是要能够获取上述示例代码中的ss来自于HTTP响应的body,为了发出HTTP请求,可以用Emacs的request库,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
(let* ((this-request (request
"http://localhost:7001/remind"
:data "{\"timestamp\":1547216100}"
:headers '(("Content-Type" . "application/json"))
:parser 'buffer-string
:type "POST"
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "data: %S" data)))
:sync t))
(data (request-response-data this-request)))
data)

此处的:sync参数花了我好长的时间才捣鼓出来——看了一下request函数的docstring后才发现,原来需要传递:synct才可以让request函数阻塞地调用,否则一调用request就立马返回了nil

现在需要的就是构造:data的值了,其中的关键是生成秒级的UNIX Epoch时间戳,这个时间戳可以通过TODO条目的SCHEDULED属性转换而来。比如,一个条目的SCHEDULED属性的值可能是<2019-01-11 Fri 22:15>,将这个字符串传递给date-to-time函数可以解析成代表着秒数的几个数字

1
(date-to-time "<2019-01-11 Fri 22:15>")

时间戳字符串要怎么拿到?答案是使用org-mode的org-entry-get函数

1
(org-entry-get nil "SCHEDULED")

PS:需要先将光标定位在一个TODO条目上。

至此,所有的原件都准备齐全了,最终我的Elisp代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(defun scheduled-to-time (scheduled)
"将TODO条目的SCHEDULED属性转换为UNIX时间戳"
(let ((lst (date-to-time scheduled)))
(+ (* (car lst) (expt 2 16))
(cadr lst))))

(defun create-remind-in-cuckoo (timestamp)
"往cuckoo中创建一个定时提醒并返回这个刚创建的提醒的ID"
(let (remind-id)
(request
"http://localhost:7001/remind"
:data (json-encode-alist
(list (cons "timestamp" timestamp)))
:headers '(("Content-Type" . "application/json"))
:parser 'buffer-string
:type "POST"
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "返回内容为:%S" data)
(let ((remind (json-read-from-string data)))
(setq remind-id (cdr (assoc 'id (cdr (car remind))))))))
:sync t)
remind-id))

(defun create-task-in-cuckoo ()
(interactive)
(let ((brief)
(remind-id))

(setq brief (nth 4 (org-heading-components)))

(let* ((scheduled (org-entry-get nil "SCHEDULED"))
(timestamp (scheduled-to-time scheduled)))
(setq remind-id (create-remind-in-cuckoo timestamp)))

(request
"http://localhost:7001/task"
:data (concat "brief=" (url-encode-url brief) "&detail=&remind_id=" (format "%S" remind-id))
:type "POST"
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "任务创建完毕"))))))

create-task-in-cuckoo中,之所以没有再传递application/json形式的数据给cuckoo,是因为不管我怎么测试,始终无法避免中文字符在传递到接口的时候变成了\u编码的形式,不得已而为之,只好把中文先做一遍url encoding,然后再通过表单的形式(form/x-www-urlencode)发送给接口了。

全文完。

Project-Euler第69题

大学的时候挺喜欢解Project Euler上的题目的,尤其是它不在乎答题者使用哪一门编程语言,甚至还有很多参与者是使用pen&paper来解题的。去年开始重新开始做Project Euler上的题目,而第69题则是最近刚刚解决的一题。惭愧的是,因为不晓得欧拉函数的计算公式(甚至都没有想过欧拉函数有没有可以用来计算的公式),所以这一题我是用暴力计算的方法来解决的。尽管花了40分钟左右才找出了问题的答案,但欧拉函数的计算方法本身还是让我觉得挺有意思的,下面我就来讲讲我在计算欧拉函数方面做的一些尝试。

69题本身很容易读懂,就是要找到一个不大于一百万的正整数n,这个n与以它为参的欧拉函数值的比值能够达到最大。欧拉函数的介绍可以看维基百科,简而言之,就是在不大于n的正整数中与n互质的数的个数,具体的例子可以参见69题描述中给出的表格。

网上可以找到别人的解法,基本的思路是:按从小到大的顺序,对于一百万以内的每个素数,都计算出它们的倍数的欧拉函数值的一部分——即对于素数p计算出1-1/p并与这个位置上原来的值相乘。当遍历完一百万以内的所有素数后,也就计算出每一个位置上的欧拉函数值,再遍历一次就可以计算出比值最大的数字了。

但我今天要讲的是笨方法。

笨方法的关键就是乖巧地计算出每一个数字的欧拉函数值。而其中最笨的,当属挑出每一个不大于n的因数,计算它们与n的最大公约数,并根据这个最大公约数是否为1,来决定是否给某一个计数器加一。示例代码如下

1
2
3
4
5
(defun phi (n)
(let ((count 0))
(dotimes (i n count)
(when (= (gcd (1+ i) n) 1)
(incf count)))))

这个phi可以稍微改进一下。例如,如果一个数a与n不是互质的,那么a的倍数(小于n的那一些)也一定不会与n互质。因此,每当遇到这么一个因数a,就知道后续的2a3a等等,都不再需要计算其与n的最大公约数了。改进后的算法代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(defun phi (n)
"通过将不互质的比特设置为1并计算为0的比特的个数来计算phi函数"
(let ((bits (make-array n :element-type 'bit :initial-element 0))
(count 0))
(dotimes (i n)
(cond ((= (bit bits i) 1)
;; 该比特已经为1,说明已经在比它小的倍数被处理时一并被标记了
)
((= i (1- n))
;; 只处理比上界要小的数字
)
((/= (gcd (1+ i) n) 1)
;; 除了当前这个不互质的数字之外,还需要将这个数字的倍数也一并处理
(dotimes (j (floor n (1+ i)))
(let* ((j (1+ j))
(m (* (1+ i) j)))
(setf (bit bits (1- m)) 1))))
(t (incf count))))
count))

为了节省内存空间,这里用了一个bitmap来标记小于n的每一个数字是否与n互质——1表示不互质,0表示互质。

其实并不需要遍历所有比n小的数字,只要遍历所有n的素因子即可。比如,将8分解为素因子,就是3个2,那么对于小于8的所有2的倍数(4和6)都是与8不互质的。基于这个方法,将所有的素因子的倍数所对应的位置为1,再数一下总共有多少个0比特即可。

对每个n都进行质因数分解效率不高,先生成一个一百万以内的素数表吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defun primep (n)
(cond ((< n 2) nil)
((= 2 n) t)
(t
(let ((bnd (truncate (sqrt n))))
(labels ((rec (test)
(cond ((> test bnd) t)
((= 0 (rem n test)) nil)
(t (rec (1+ test))))))
(rec 2))))))

(defvar *primes-in-1000000* nil)
(defun generate-primes-in-1000000 ()
(dotimes (i 1000000)
(when (primep (1+ i))
(push (1+ i) *primes-in-1000000*)))
(setf *primes-in-1000000* (nreverse *primes-in-1000000*)))

然后对于一个给定的n,遍历所有小于它的素数并对相应的倍数所在的比特置一就可以了,示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defun phi3 (n)
"直接用素数表来做筛法"
(prog
((bits (make-array n :element-type 'bit :initial-element 0)))
(dolist (num *primes-in-1000000*)
(cond ((> num n)
(go :return))
((zerop (mod n num))
(labels ((aux (i)
(when (< i n)
(setf (bit bits i) 1)
(aux (+ i num)))))
(aux (1- num))))))
:return
(return-from phi3
(count-if (lambda (bit) (zerop bit)) bits))))

PS:写这个phi3的时候发现Common Lisp提供了一个prog宏,这个宏倒是真的挺好用。

改进了两轮,其实这仍然是笨方法。即便是用phi3,用来计算题目的答案也花了40多分钟。

全文完

一些在Emacs中搜索文本的方法

在Emacs中写代码的时候,常常需要查找一个函数、方法,或者变量的定义。如果是正在写Common Lisp,那么SLIME已经配置好了相应的快捷键M-.,只需要将光标移动到要查看的函数、方法,或者变量的名字上,按下M-.便可以跳转过去——再按一下M-,还能回到原来的位置。

如果是写其它语言的代码,很多时候都没办法方便地跳转过去,这时候就需要依赖于文本搜索了,这也是本篇所要讲述的主题。

通常情况下,用C-sC-r就足够了——一个负责“往下”搜索一个负责“往上”搜索。尤其在安装了Emacs的插件swiper之后,只需使用C-s便可以同时查看到上下两个方向的匹配文本。

C-s也有其局限性。例如,它不能跨文件搜索,如果要查看的函数、方法,或者变量的定义不在当前buffer中,就不得不手动在多个buffer间切换并频繁按下C-s了。

有多种办法可以解决上面这种问题。例如,可以用Emacs的projectile-ag。通常,如果代码散布在多个源文件中,那么它们多半是放在一个项目中——比如一个Git仓库。打开位于项目中的文件时,Emacs的projectile-mode就会启动。此时,按下C-c C-p s s这套组合键,会调用projectile-ag函数。projectile-ag会在minibuffer中等候输入要搜索的内容,按下回车后,Emacs会调用命令行工具ag来搜索这个项目下的所有文件,找出匹配关键字的行并显示。

projectile-ag函数会打开另一个buffer来展示搜索结果,一个示例如下

1
2
3
4
5
6
ag --literal --group --line-number --column --color --color-match 30\;43 --color-path 1\;32 --smart-case --stats -- emacs .
0 matches
0 files contained matches
36 files searched
111365 bytes searched
0.007795 seconds

使用projectile-ag的前提是要搜索的文件都在同一个一个项目中,但并非所有时候都满足这个要求。这时,可以用Emacs的find-grep函数。

find-grep函数调起后同样要求使用者在minibuffer输入内容,但它更原始一点

光标会定位在-e选项之后,需要填补交给grep的正则表达式。由于minibuffer中给出的是完整的、将会被运行的命令,因此可以也给find命令添加一些选项和参数,来改变搜索行为。

如果是在一个Node.js项目中搜索,一般还要让find忽略一些文件,如node_modules目录下的大量依赖,或者构建产生出来的.css和.js文件。这些文件中的行不仅很可能会命中输入的正则表达式,还极可能成片地出现,占据搜索结果中的半壁江山。

除了grep之外,还有许多命令行的文本搜索工具,例如ackrg,并且它们都称自己更快。要在Emacs中使用它们也很简单,尤其是后者还有相应的插件rg.el可以方便调起。

如果经常要控制find来忽略node_modules,可以考虑用git-grepman git-grep中说到,它只会搜索tracked的文件

git-grep的man文档

node_modules一般都不会被git跟踪,自然也就不会被搜索。

全文完

Emacs的org-mode实现自动的internal archive

缘由

org-mode是一个Emacs内置的major mode,当打开一个后缀为.org的文件时就会被启用。在官网的介绍中提到,它可以用于管理待办事项,而这也正是我目前使用org-mode最多的场合。比如,我用它来记录漫画的阅读进度,每一话或每一章就是一个标记了TODO关键字的条目,读完那一话或那一章后就会将对应的条目标记为DONE。一般我会一周一次地归档自己标记为DONE的条目,但由于一次要处理的条目可能很多,逐一将它们归档比较繁琐,因此,便打算二次开发实现一个自动归档的功能。

需要事先声明的是,本文不是org-mode的入门教程,也不会讲解如何配置org-mode。对这方面有兴趣的读者,可以自己搜索一番,资料还是相当丰富的。

菜谱

对于每一个被完成的单独的阅读任务,我的做法是将其internal archive。等到一整本漫画都读完之后,再将整个以漫画名命名的条目归档到别的文件中去。要实现自动的internal archive,最简单直接的办法是借助于org-mode提供的各种hook。

org-mode提供了许多的hook,在官方的文档中有一一列举 。其中,名为org-after-todo-state-change-hook的便是我所需要的钩子。只需往这个变量所绑定的列表中添加一个函数,那么这个函数便会在条目切换状态时(比如从TODO切换到DONE)被org-mode调用。

最终的ELisp代码如下

1
2
3
4
5
6
7
8
(defun lt-archive-if-manga ()
(let ((state org-state))
(when (string= state "DONE")
(let ((tags (org-get-tags-at)))
(when (member "漫画" tags)
(org-toggle-archive-tag))))))

(add-to-list 'org-after-todo-state-change-hook 'lt-archive-if-manga t)

稍微解释一下。从C-h v org-after-todo-state-change-hook RET的文档可以得知,条目的新状态可以通过变量org-state获取。取得新状态(是个字符串)后,首先检查其是否为"DONE"。如果是,再检查这个条目是否为一个阅读漫画的任务。

在我的用法中,凡是漫画条目,都打上了名为"漫画"的标签。因此,使用函数org-get-tags-at取得一个条目的所有标签(包括从父级条目继承下来的),再用member函数判断这些标签中是否包含字符串"漫画"。如果有,就调用org-toggle-archive-tag将该条目internal archive。

传给函数add-to-list的第三个参数t的作用,是让这个新加入钩子的函数最后被调用。

全文完

2018年度技术总结

转眼间2018年只剩下最后的几天了,赶紧趁热写篇年度总结,毕竟据说元旦会变冷。

入手Mac

参加工作的第五个年头,终于买了一台自己的MacBook Pro。其实我从高中时起,就对Mac有一种憧憬。那时候每到周末,就常常往苹果的实体店跑,就为了去看看那些精致的笔记本,试着在触控板上滑动一下手指,点开几个自己只在苹果官网上看过icon的陌生应用。

依稀记得下决心买这台电脑的那天晚上,回到家掏出之前的联○笔记本,发现转动显示屏盖子的地方坏掉了,导致笔记本的盖子翻不起来。在家里要接一个外置的显示器来用实在是太麻烦了,立即就萌生了买一台新的来应急的想法。再三思索后,决定尝试一下Mac,便立即在官网下单了。令人哭笑不得的是,明明是要应急用的,结果还是过了三天才到手。庆幸的是,在公司是用外置显示器来办公的。

用上Mac之后有挺多的感触,有兴趣的读者可以移步这里阅读,这里就不再赘述了。令我自己也感到惊讶的,是我在用了Mac之后还购买了几款软件——我并不是一个很舍得花钱买软件的人,多数情况下,都是用一些免费的开源软件的。在Mac上买的这几款软件,大概是因为它们真的挺好用吧。最早入手的是Alfred,买了它的Powerpack。后来买了BetterTouchTool,自定义了很多touchbar上的按钮,用得挺欢的

BetterTouchTool的使用现状

再后来,遇到了堪称神器的Contexts,现在在macOS中切换窗口就像是牛奶巧克力那般的丝滑。最近买的,则是Bartender,是在淘宝上的数码荔枝那里买的,趁着双十一的时候有折扣赶紧入了手。它们都很实用,使用频率也非常地高。当然了,像Default Folder X虽然也非常好用,但因为它可以无限期地免费使用(只是会偶尔弹个窗提醒购买),所以我就没有急着花钱了。一些比较有意思的应用,比如Little Snitch,虽然很酷炫(看着世界地图上的各种连线),但对我而言用处不大,最后也就卸载了。

写博客

越是写博客就越发现,博客的力量是有限的,除非超越博客。我不做程序员啦JOJO比起写给自己查阅的笔记,写公开发表的文章是大不同的。笔记可以写得像铜墙铁壁那么规整,可以一层一层地嵌进去。但是发表在博客上的文章就像代码,是写给自己之外的人看的,要讲究阅读体验。偶尔要用段子活跃一下气氛给读者提提神,字里行间也要注意正确地使用行话。尤其是写一些教程一般的文章时,要循序渐进地讲述自己的操作过程,还要战战兢兢地担心别人无法复现自己的结果(人类的本质是复读机)。

重新开始写作后才发现,简书上的最后文章已经是2017年七月份的了。重开的博客,打算继续发表在GitHub Pages上。本来GitHub Pages上的博客的页面,是我用自己写的一个工具来生成的。结果这个半成品在Mac上因为cl-mysql安装失败跑不起来,我也一时不想折腾,于是决定换个成熟的工具来用。目前用的是Hexo。一个惊喜是,Hexo默认支持Google Analytics——尽管并没有多少人会去看我的博客。

除了GitHub Pages之外,我也把文章发表到了SegmentFault的专栏上。感谢SegmentFault极其不友好的插入图片的方式,迫使我写了一个Alfred的Workflow,用来快速地把截图的图片上传到GitHub的一个仓库里(拿GitHub的仓库当图床)。现在的写作流程,是在电脑上用Typora先写好,然后hexo new一下生成源文件,把写好的内容粘贴进去,再发布,最后把文章内容再到SegmentFault上创建篇新文章再贴一次,发表出去。

布谷,布谷

以前用(坏掉现在又修好了的)联○笔记本的时候,我用Windows 10自带的Alarm设置了很多提醒——叫外卖的、喝水的,以及起来走走的(久坐是不好的哟),大量的定时提醒让我有一种生活井井有条的感觉——写作感觉读作错觉。Mac在这方面可以做得更好,因为它自带crontab。于是我便用crontab和alerter(刚开始的时候用的是terminal-notifier)给自己设定了不少定时提醒。等到crontab -l的输出开始泛滥后,便萌生了自己写一个管理工具的想法。

一开始还在Boostnote上煞有介事地写了一篇需求文档和设计文档(已经都是废稿了),想着用Common Lisp来开发。但同样因为cl-mysql安装不成功,我又不希望把时间都花在了折腾环境上,便改用了Node.js来编写这个管理工具。框架选择了egg-js,在操作MySQL和Redis方面都有相应的插件,此外还内置支持定时任务,上手很方便——真要是用Common Lisp的话,也许还在纠结某个功能是用某个半残的第三方库还是自己费劲从零写起。

用Redis的ZADD、ZRANGEBYSCORE、ZREM,以及ZSCORE指令做了一个简陋但够用的消息队列——用Z*系列的指令是为了可以模拟出延时消息的效果(beanstalkd阿里云MNS都支持这种特性)。配合egg-js的定时任务功能,就可以实现定时提醒了——弹出提醒仍然是用alerter。目前这套系统运作得还不错,大部分原本录入在crontab中的定时提醒已经交由它来处理了。尽管还有不少的小问题,不过相信都是可以解决的。

对了,这玩意儿的名字叫做cuckoo,即布谷鸟。

GTD?

Mac跟“效率”这个词似乎特别有缘,常常被人换做生产力工具,仿佛一拿起Mac,便自动屏蔽了外界的干扰。开始用Mac的几天后,我便开始把玩macOS上各款大名鼎鼎的TODO list应用了。关于这个话题之前也写了一篇吐槽文,有兴趣的可以移步这里阅读。世间的TODO list应用是真的多,不过可能是我的口味实在是太刁钻了,我竟然没有一款是特别满意的。在把玩的期间最让我产生好感的,要属My Life Organized,然而这货没有Mac版,不然我真的很可能会喜加一。

每过一段时间,我就会想要把自己对TODO list类应用的一些想法付诸实践,自己动手开发一个给自己用。不过到目前为止,这些想法仍然处于被封存的状态,被遗忘在了磁盘上哪个角落里的文件中。目前Emacs的org-mode还算够用,它兼顾了我使用上的凌乱与规整,尤其是当我需要在某个任务下写一些包含代码的笔记或者想法的时候,org-mode几乎就是所有TODO list类应用中的唯一选择了。但工具只是用来管理任务,当夜深人静坐下来,想要自己第二天给安排得明明白白的时候,就会发现,即便有最好的工具(我并不是说org-mode),也仍然需要方法论来指导这个安排的过程。尤其是,这个过程应当是“object-oriented”的——不是面向对象,而是“目标导向”。如果不事先制定一些目标——不管是像人生规划这般空泛的目标,还是像租一辆共享汽车开车上路这样具体的短期目标,如果缺乏目标,那么很快就会陷入了“随便找一些任务来填充第二天的空闲时间”这样的状态,久而久之GTD也就实践不起来了。

规划不等于目标。

Note-taking

无法高亮编程语言代码的Evernote、OneNote,使用不通用的存储格式的Boostnote、Quiver,还有收费的为知笔记,都没能够取代Emacs的org-mode成为我做笔记的工具。org-mode最弱的地方,就在于使用起来不够随意,不像其它的几款笔记软件那样,截图之后的图片没有办法一键粘贴到.org文件中去。但恰恰我个人不太喜欢截个图配一段话的笔记形态,所以这个缺点可以视若无睹。我现在的笔记都是QA形式,一个一级headline就是一个问题,headline下的文本就是答案,而org-mode又支持嵌入代码(虽说Markdown也支持),很适合我的习惯

Emacs中的笔记示例

最近我觉得,与记笔记同样重要的,是能够方便并且准确地查找自己的笔记。笔记如果只是记而没有翻阅出来利用,那还不如每次都打开搜索引擎当场查找算了。我打算把笔记的导入到ElasticSearch中去,然后依托它的全文搜索功能来查找。感谢org-mode,是纯文本的存储格式。要写一个工具,把.org文件中的每个问题和对应的答案组装成一个JSON喂给ElasticSearch真是太简单了。现在缺的是一个方便的入口,以及一个美观大方的结果显示方式。

不过这个新想法的项目名还没想好

Web后端的固有结界

年初开始渐渐负责起了面试的工作。为了可以比较系统地面试,便整理了一份Web后端工程师需要掌握的知识的清单。目前这份清单还在绝赞完善中——想必这个完善的过程是不会停止下来的,而且目前积累的面试题也不足。

原本还有另一份清单,是自我提升用的指引。但渐渐地我发现要求面试者所具备的知识,和充电用的技能树指引,其实是应当合二为一的,于是乎便诞生了一个叫做charging的项目。在其中的一个叫做knowledge.org的文件中(又是org-mode),我以自己的理解自上而下地给Web后端的软件工程师所需要的知识做了一下划分,并逐级细分,到了合适的粒度的headline,便添加这个分类下的相关面试题。除了在这些叶子节点上挂上面试题之外,我还依照这些合适粒度的headline给自己安排学习的内容,一般是相关主题的电子书或者PDF。经过最近一次的梳理后,接下来可能会学习一下Erlang(都不记得是第几次了),读一下《重构》,以及《Redis实战》。当然了,这些只是最近一次整理增加的内容,仅仅是完整学习内容的冰山一角XD

我本来不喜欢听网课的,认为视频和语音方式的教学,接收信息的效率比用眼看的方式要来得低效,毕竟不管是视频还是音频,总是要收完前一段内容才能继续收下一段内容(真香警告)。大约在两周前,买了极○时间的专栏《MySQL实战45讲》,听下来发觉其实还挺有意思,尤其适合在通勤和夜晚慢跑时听,算是2018年新增的一种学习方式吧。

后记

尽管有年月日的划分,但日子毕竟是连在一起过的,所以今年未完成的学习安排并不会在2019年到来的那一刻戛然而止。Org Agenda中还有很多标记为TODO的条目,Pocket中还有很多未读的文章,还有很多没看完的PDF,LeetCode和Project Euler上也还有很多的题目没做。2019年,想必会是忙碌的一年。

全文完

如何随机挑选要做的任务

背景

由于个人喜好的因素,选择了用Emacs的org-mode来实践GTD,管理自己的任务和安排日程。但也因为个人喜好的因素,导致在安排第二天的计划,从积累的TODO列表中挑选要做的事情时,总会下意识地跳过一些一看就很麻烦的任务。久而久之,在列表的顶部,便堆积着一些好久前就创建的TODO。而因为总是从上往下挑选任务,列表的底部则是堆积着好久没有露脸的TODO。有不见者,三十六年。

迫切需要一个办法来解决这个难题,但若真的一丝不苟地从上往下处理每一条TODO又觉得没意思,该怎么办?不如从中随机地挑选TODO来安排到第二天的日程中?Sounds good。

如何实现

一开始,我是打算找找现成的类似功能的,不过放狗搜了一番后并没有什么收获。之后的某天我忽然意识到,.org文件不过就是普通的文本文件而已,直接用命令行工具处理就好了呀。摸索一番之后才知道并不难,成果就是下面这段简单的shell命令

1
find . -name '*.org' ! -name 'trash.org' ! -name 'work.org' -exec grep -Hn '\*\* TODO' {} \; | sort -R | head -3

稍微解释一下。首先登场的是find,它用来遍历目录下的所有.org文件——因为我把TODO按照不同的领域放到了不同的.org文件下。传递给find的参数的意思,是“匹配所有文件名以.org结尾、但既不叫trash.org、也不叫work.org的文件”

1
-name '*.org' ! -name 'trash.org' ! -name 'work.org'

trash.org是垃圾箱,work.org存放的是工作相关的任务——我可不喜欢把工作安排到自己的闲暇时光里。

通过-execfind调用grep从.org文件中过滤出符合条件的带有TODO关键字的行——在我的.org文件中,有很多行是没有TODO关键字的非任务型的内容,它们可能是一个目标、一个分类,甚至可以是某个TODO条目下的“笔记”。用下面的正则即可筛选出想要的内容

1
'\*\* TODO'

总和运用findgrep后,便到了从中挑选的环节了。虽然开始的时候提到的是“随机地挑选”,但可以参考音乐播放器的“随机播放”功能的做法,即先将所有的TODO条目随机排序,然后从头开始按顺序取出前几个。sort命令的-R选项已经实现了随机排序,再用head选取前3个即可。

全文完

用过的一些Markdown编辑器

开篇

买了MacBook Pro之后的一段时间里,为了打造适合自己的知识管理体系,折腾起了笔记类软件(题外话,我还挺喜欢尝试新软件的,尤其在接触macOS后发现许多软件都长得很漂亮)。其实在入手Mac之前,我已经试用过不少笔记类软件和服务了,包括Evernote(还有印象笔记)、有道云笔记、为知笔记,等等。再后来,改用Emacs的org-mode来写笔记——主要是将一些常常搜索的内容或经验记录在了多个.org文件中,算是一份自己的FAQ。后来想看看在macOS的世界中有没有更好的工具,同时渐渐觉得Markdown是一个更好的笔记内容载体,便尝试了一些知名的笔记类软件暨Markdown编辑器。

大致上尝试了下列这些:

  • Emacs
  • Boostnote
  • Quiver
  • Typora
  • Visual Studio Code
  • Yu Writer

本文并不是一篇完整的、专业的软件评测报告,只是我兴趣使然的对各个软件的吐槽和赞美,各位权当打发时间吧。下面我按顺序说一下上面提及的各款软件。

Emacs

Emacs并不仅仅是一款Markdown编辑器,我用得最多的是用它来做计划(之前还用来写Node.js代码,不过现在交给VSCode了)。用Emacs来写Markdown,坏处是没有live preview的功能。在Emacs中打开了一个.md文件,只会原原本本地显示着井号、星号,三个反引号等Markdown语法的关键字——并且还是白底黑字的模样,而不带有丝毫不同的样式。为了让它们好看点,你还需要安装一个叫做markdown-mode的Emacs扩展。但几遍安装了markdown-mode,也无法实时预览。markdown-mode的菜单栏中有一个叫做“Preview”的功能,它依赖一个名为markdown的命令行工具(用brew install markdown可以安装)。当一切安装完毕点击“Preview”菜单项时,才发现是在网页浏览器中查看的方式——虽然有preview了,但并不live。

Emacs在写Markdown方面也并非一无是处。对程序员而言,在一篇Markdown写就的文章中插入代码是再正常不过的事情了。在Emacs中将光标定位到Markdown语法的代码块内,按下controlc的组合键,再敲一下单引号键,Emacs便会另起一个相应模式的buffer,并将代码块中的内容复制到新buffer中供继续编辑。如下图所示

Emacs_编辑Markdown代码块.gif

在上面的GIF中,代码块以GitHub Flavored Markdown的语法在开头的三个反引号后附上了模式的名字,即lisp,Emacs便会打开lisp-mode的buffer。在这个buffer中可以继续使用Emacs的完整功能编辑对代码,包括语法高亮、自动补全,等等——如果启动了SLIME,甚至可以运行里面的Common Lisp代码。

Emacs和VSCode用于在编写代码的同时写写项目的README.md文件应当是绰绰有余的了。

Boostnote

Boostnote自诩为“程序员的笔记本”,它并不是我在Emacs之外寻找的第一款笔记软件,在它之前,我还尝试了NotionQuiver来着。上手后发现,Boostnote简直就是Quiver的开源免费版本,相当的喜爱。

Boostnote当然让我格外喜欢的有几点:首先,Boostnote可以实时预览键入的Markdown源文档。会有一列跟编辑区域差不多宽的区域被用来展示Markdown渲染后的效果。(刚刚发现,原来这个区域的宽度是可以拖动调节的)

其次,它不仅支持Markdown、带语法高亮的代码块,甚至还支持表格和流程图的绘制!当然我以为,用竖线和连字符绘制表格的功能仅在Emacs的org-mode中存在(孤陋寡闻了汗颜),刚开始用Boostnote制作表格的时候可是相当兴奋。而text-based的绘制流程图的方式也是让我大开眼界(后来才知道原来有flowchart.js这样的工具)——尽管后来我渐渐发现,绘制流程图其实挺少用。

然后Boostnote具备在多份笔记中搜索的功能,这对于一款笔记软件而言倒是真的非常重要,因为有时候只能想到一些只言片语,而并不能确定所要查阅的内容究竟在哪一份笔记中。

但Boostnote也有一些缺点。首先,Boostnote是用自有的文件格式(而不是纯文本的.md文件)来存储输入的内容的——打开~/Boostnote/notes/可以看到这些后缀为.cson的文件。这样一来,假设我日后发现了一款更优秀的Markdown编辑器,那就不能无痛迁移了,还得先从Boostnote中将这些笔记逐一导出成.md文件才行。

其次,Boostnote只支持三层的组织结构——最外层是storage,然后是folder,最后就是笔记本身。当初有道云笔记特别让我喜欢的,就是它支持非常多层级的目录结构。尽管目录不是越多越好,但有这种灵活性总是更好的。否则,笔记的使用者就只能在命名和标签上下功夫了

最后一点,就是Boostnote在我的系统上非常容易崩溃。有时候一翻起盖子,看到的就是Boostnote崩溃的提示。

不过Boostnote支持往其中粘贴图片,当我需要快速记录一些图文内容时,我还是很喜欢用它的。

Yu Writer

某一天偶然遇到了Yu Writer,它官网上的截图看着很吸引人,于是我便试用了一下。第一印象是,Yu Writer is awesome!首先它很人性化。它的预览区域是一个minimap——就是Sublime Text最右侧的那一列。在做到实时预览的时候,也不会占用太多的横向空间。其次,它支持大纲视图

Yu Writer的大纲视图

即上图左侧的目录。恰逢当时我在用Boostnote写一篇比较长的设计文档,深刻地体验到了一个大纲视图的重要意义——对在长文档内的多个标题间跳转非常有帮助。再次,Yu Writer还准备了工具栏,方便不懂得Markdown语法的用户;支持标签页,便于在多个文档间切换;甚至可以把一个Markdown文档作为幻灯片来播放。

但Yu Writer也有它自己的劣势。第一,在Yu Writer内,原本在macOS系统中全局可用的Emacs风格快捷键——即control+b往左移动光标、control+f往右移动光标——居然不生效!这些快捷键对我个人还是非常重要的。

第二,在Yu Writer中,不能直接插入磁盘上的图片文件的绝对地址,既没有在预览区域显示出来,也没有在文档列表显示成功。

据说Yu Writer的作者的主业是厨师,感觉好强

Typora

Typora is best。不同于前面提到的几款Markdown编辑器,Typora是“所见即所得”的编辑器。你敲入两个井号,加一个空格,再敲入你的标题内容,最后回车,那么标题内容就会被渲染为二级标题的形式,如下图所示

Typora_输入二级标题

这么一来,屏幕上的空间基本都可以被用来写作,不需要担心被预览用的列给占据了。

然后,Typora没有自定义它的存储结构,它直接打开磁盘上的.md文件进行编辑,这些Markdown源文件可以随心所欲地放在任何喜欢的目录下,只要能打开就行。再加上它文件树视图,就实现了不受限制的笔记组织方式了,如下图

Typora_文件树视图

不过一个可以想到的缺点,就是Typora不支持在所有的Markdown文件上搜索关键字——毕竟它也不知道要去哪个目录下寻找这些待搜索的源文件。

尽管Typora外观很简洁,但Boostnote有的功能它一个也没有落下,就像它的官网所说的那样

Typora的官网宣传

现在我的博客的文章基本都是用Typora来写的,冥冥中感受到了一股乐趣。但Typora毕竟没有搜索功能,所以我又开始摸索额外的搜索笔记的方式了(比如把记录在.org文件中的FAQ导入到ElasticSearch中再借助全文搜索的力量来找到自己要的内容)。

后记

没有最好的,只有最适合的,祝各位都能找到最适合自己的Markdown编辑器。

把GitHub作为图床

背景

最近又迷恋上了写博客,尤其是前一段时间很想要写点东西分享一些软件的使用感想。但当写完文章想要发表时就会碰到一个问题:由于我是现在本机的编辑器中用Markdown写好了全文的内容,再发表到各个平台(曾经是GitHub Pages搭建的博客,后来又多了简书,现在再加上SegmentFault)上的,因此文章里的图片都是引用在本地磁盘上的文件路径的。这么一来,如果直接将文章源码粘贴到博客平台上——比如粘贴到SegmentFault中,那么这些本地的图片链接就无法在发布后的文章中正常显示了。

如果一开始就在SegmentFault中写作也会遇到问题。SegmentFault上的文章插入图片后,并不是像普通的Markdown源码那般插入一条![]()形式的标记的,而是像下图这样

在SegmentFault中插入图片后的效果

显然,这样的文章源码复制到其它平台(GitHub Pages、简书)去发布的话,必然是需要针对其中的图片标记修改一番的——比刚开始的方法或许要更麻烦。

看来要解决这个图片链接在不同平台间共用的问题,必须有一处纯粹的用于存放图片文件的地方——也就是大家常说的图床了。刚开始我也放狗搜了一下,看看别人的推荐,印象中得到的答复不外乎是又○云、七○云、新○微博,以及sm.ms等。但它们要么需要注册并且实名认证,要么不纯粹,要么让人觉得随时会丢失。

某个晚上忽然想到,GitHub不就是一个很好的图床么?!在GitHub上建一个仓库专门存放博客中的图片,不仅免费、完全受自己管理,而且自带CDN加速,并且我的读者群(如果真的有这么一个群体的话)也应当可以畅通地访问GitHub。

放图片的仓库虽然有了,但用起来还不是很便利——因为作为写作素材的图片在我的电脑上是存放在一个单独的、非GitHub仓库的目录下的,所以如果要丢到图床上,就需要先将文件复制过去,然后执行git的add、commit、push三部曲,最后还要到GitHub上复制这张新图片的“raw”地址。

这个过程很机械化,完全可以用一个AlfredWorkflow来代劳。

编写Workflow

编写Workflow就像编写Common Lisp中的宏一样,总是从它们的用法入手的。在我的设想中,这个Workflow的使用方式应当是:

  1. 首先,按下快捷键调出Alfred的输入框,输入关键字(在我这里就叫做upload)来唤起这个Workflow;
  2. 然后,输入要上传的图片文件的绝对路径并按下回车,开始在后台处理
  3. 最后,上传完毕后,弹出通知来告诉我

整个Workflow的概貌其实很简单

upload Workflow的全貌

第二个节点所调用的External Script是长这样子的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash
# 将磁盘文件上传到GitHub

path=${1}

pictures_dir="${HOME}/Documents/Projects/riverbed/pictures"
cp "${path}" "${pictures_dir}"
echo '文件复制完毕'

file=$(basename "${path}")
cd "${pictures_dir}"
git add "${file}"
git commit -m '上传一张图片'
git push -u origin master
echo '文件已提交到GitHub'

/usr/local/bin/node -e "console.log(encodeURI('https://raw.githubusercontent.com/Liutos/riverbed/master/pictures/${file}'));" | tr -d '\n' | pbcopy

获取文件的绝对路径其实很简单,在Finder中选中文件后,按下Command+Option+C即可

这里使用basename命令获取文件名。并且,为了避免git打开文本编辑器要求输入commit message,向git-commit命令传递了-m选项。

因为文件名含有非ASCII的字符(毕竟会有中文),需要做一次URL编码,因此用了node来做转换。在Node.js代码中用console.log输出编码后的图片URL,结尾会有一个换行符,所以用tr将其去掉。最后,输出的内容重定向给pbcopy,就将上传后的图片URL复制到剪贴板中了。如果此时正在编辑文章,便可以粘贴这个图片的链接到源码中。

Alfred也提供Copy to Clipboard,用于将Workflow中上一个节点的输出复制到剪贴板中。之所以不使用,其实是因为刚开始的时候就是用的Alfred的Copy to Clipboard,结果发现git运行过程中的输出也被Alfred接收了,跟图片URL一起混进了剪贴板中。所以最后改为直接调用pbcopy

全文完。

编写嵌套反引号的宏

一个没事找事的例子

当在Common Lisp中定义宏的时候,常常会使用到反引号(`)。比方说,我有这么一个函数

1
2
3
4
(defun foobar ()
(+ 1 1)
(+ 2 3)
(+ 5 8))

它被调用后会返回最后一个表达式的结果——13。如果我希望在第二个表达式计算后就把结果返回给外部的调用者的话,可以用return-from

1
2
3
4
(defun foobar ()
(+ 1 1)
(return-from foobar (+ 2 3))
(+ 5 8))

当然了,这属于没事找事,因为完全可以把最后两个表达式放到一个prog1(这也是没事找事),或者直接点,把最后一个表达式删掉来做到同样的效果——但如果是这样的话这篇东西就写不下去了,所以我偏要用return-from

还有一个更加没事找事的办法,就是用macrolet定义一个局部的宏来代替return-from——我很想把这个新的宏叫做return,但这样SBCL会揍我一顿,所以我只好把这个宏叫做bye(叫做exit也会被揍)

1
2
3
4
5
6
(defun foobar ()
(macrolet ((bye (&optional value)
`(return-from foobar ,value)))
(+ 1 1)
(bye (+ 2 3))
(+ 5 8)))

如果我有另一个叫做foobaz的函数

1
2
3
4
(defun foobaz ()
(+ 1 2)
(+ 3 4)
(+ 5 6))

也想要拥有bye这种想来就来想走就走的能力的话,可以依葫芦画瓢地包含一个macrolet

1
2
3
4
5
6
(defun foobaz ()
(macrolet ((bye (&optional value)
`(return-from foobaz ,value)))
(+ 1 2)
(bye (+ 3 4))
(+ 5 6)))

好了,现在我觉得每次都需要在函数体内粘贴一份bye的实现代码太麻烦了,想要减少这种重复劳作。于是乎,我打算写一个宏来帮我复制粘贴代码。既然要定义宏,那么首先应当定义这个宏的名字以及用法,姑且是这么用的吧

1
2
3
4
(with-bye foobar
(+ 1 1)
(bye (+ 2 3))
(+ 5 8))

with-bye这个宏需要能够展开成上面的手动编写的foobar中的函数体的代码形式,那么with-bye的定义中,就一定会含有macrolet的代码,同时也就含有了反引号——好了,现在要来处理嵌套的反引号了。

这篇文章有个不错的讲解,各位不妨先看看。现在,让我来机械化地操作一遍,给出with-bye的定义。首先,要确定生成的目标代码中,那一些部分是可变的。对于with-bye而言,return-from的第一个参数已经macrolet的函数体是可变的,那么不妨把这两部分先抽象为参数

1
2
3
4
5
(let ((name 'foobar)
(body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
`(macrolet ((bye (&optional value)
`(return-from ,name ,value)))
,@body))

但这样是不够的,因为name是一个在最外层绑定的,但它被放在了两层的反引号当中,如果它只有一个前缀的逗号,那么它就无法在外层的反引号求值的时候被替换为目标的FOOBAR符号。因此,需要在,name之前再添加一个反引号

1
2
3
4
5
(let ((name 'foobar)
(body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
`(macrolet ((bye (&optional value)
`(return-from ,,name ,value)))
,@body))

如果你在Emacs中对上述的表达式进行求值,那么它吐出来的结果实际上是

1
2
3
4
5
(MACROLET ((BYE (&OPTIONAL VALUE)
`(RETURN-FROM ,FOOBAR ,VALUE)))
(+ 1 1)
(BYE (+ 2 3))
(+ 5 8))

显然,这还是不对。如果生成了上面这样的代码,那么对于bye而言FOOBAR就是一个未绑定的符号了。之所以会这样,是因为

  1. name在绑定的时候输入的是一个符号,并且
  2. name被用在了嵌套的反引号内,它会被求值两次——第一次求值得到符号foobar,第二次则是foobar会被求值

因此,为了对抗第二次的求值,需要给,name加上一个前缀的引号(‘),最终效果如下

1
2
3
4
5
(let ((name 'foobar)
(body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
`(macrolet ((bye (&optional value)
`(return-from ,',name ,value)))
,@body))

所以with-bye的定义是这样的

1
2
3
4
(defmacro with-bye (name &body body)
`(macrolet ((bye (&optional value)
`(return-from ,',name ,value)))
,@body))

机械化的操作方法

我大言不惭地总结一下,刚才的操作步骤是这样的。首先,找出一段有规律的、需要被用宏来实现的目标代码;然后,识别其中的可变的代码,给这些可变的代码的位置起一个名字(例如上文中的namebody),将它们作为let表达式的绑定,把目标代码装进同一个let表达式中。此时,目标代码被加上了一层反引号,而根据每个名字出现的位置的不同,为它们适当地补充一个前缀的逗号;最后,如果在嵌套的反引号中出现的名字无法被求值多次——比如符号或者列表,那么还需要给它们在第一个逗号后面插入一个引号,避免被求值两次招致未绑定的错误。

一个例子

就用上面所引用的文章里的例子好了。有一天我觉得Common Lisp中一些常用的宏的名字实在是太长了想要精简一下——毕竟敲键盘也是会累的——假装没有自动补全的功能。我可能会定义下面这两个宏

1
2
3
4
(defmacro d-bind (&body body)
`(destructuring-bind ,@body))
(defmacro mv-bind (&body body)
`(multiple-value-bind ,@body))

显然,这里的代码的写法出现了重复模式,不妨试用按照机械化的操作手法来提炼出一个宏。第一步,先识别出其中可变的内容。对于上面这个例子而言,变化的地方其实只有两个名字——新宏的名字(d-bindmv-bind),以及旧宏的名字(destructuring-bindmultiple-value-bind)。第二步,给它们命名并剥离成let表达式的绑定,得到如下的代码

1
2
3
4
(let ((new-name 'd-bind)
(old-name 'destructuring-bind))
`(defmacro ,new-name (&body body)
`(,old-name ,@body)))

因为old-name处于嵌套的反引号中,但是它是由最外层的let定义的,所以应当添上一个前缀的逗号,得到

1
2
3
4
(let ((new-name 'd-bind)
(old-name 'destructuring-bind))
`(defmacro ,new-name (&body body)
`(,,old-name ,@body)))

最后,因为old-name绑定的是一个符号,不能被两次求值(第二次是在defmacro定义的新宏中展开,此时old-name已经被替换为了destructuring-bind,而它对于新宏而言是一个自由变量,并没有被绑定),所以需要有一个单引号来阻止第二次的求值——因为需要的就是符号destructuring-bind本身。所以,最终的代码为

1
2
3
(defmacro define-abbreviation (new-name old-name)
`(defmacro ,new-name (&body body)
`(,',old-name ,@body)))

试一下就可以确认这个define-abbreviation是能用的(笑

后记

能够指导编写宏的、万能的、机械化的操作方法,我想应该是不存在的

重复输入相似的命令的几种方法

在命令行经常需要重复输入一些shell代码,例如用cd切换到某个目录、运行npm run local,或者git commit等。每次都完整地一个个字符地敲入这些命令还是很麻烦的,这种时候就要寻找可以解决重复输入,提高效率的办法了。

最原始的,当然是找一个文本文件,把平时经常敲入的命令存放在其中,每当需要运行这些命令的时候就打开文件选中内容复制一下,再到终端粘贴并运行,但这未免过于原始了。

使用ctrl-r翻出历史命令

使用ctrl-r是一种不那么原始的方法。在终端中按下ctrl-r后,shell会等待进一步地输入,并根据输入从以前输入过的命令中找出匹配的一条。找到了自己所需要的命令行,直接敲击回车即可,效果如下
ctrl-r的效果
PS:上面是使用了fzf之后的效果,所以在敲入回车后并不会立即执行所选中的命令。原生的ctrl-r命令不支持在不同的位置上匹配输入字符,所以还是推荐一试fzf的。

使用alias

alias相比于ctrl-r而言进化了一点,因为它毕竟不再需要往命令行中塞入那么多字符了——它让终端用户可以用较短的内容来代替较长的内容。例如,我就给登录我本地的MySQL的命令写了一个alias

1
alias myroot='mysql -u root -p*******'

而且alias更像是宏展开,所以可以在后面添加其它内容,如下图
alias的效果
在myroot之后输入的test和user_info都跟着myroot展开后的结果一起喂给了shell去执行。使用alias之后,每次只需要输入较短的myroot即可。

使用函数

如果说alias是C语言里面的宏的话,那么shell所支持的函数就是C语言里面的函数了(这不是废话么)。alias始终不太适合所要输入的内容比较多的场景——定义也特别难写,并且alias没有输入参数可言,也不适合处理需要有为妙差异的重复内容的情况。shell函数很适合这种情况,例如,我在本地编辑完一个.sd文件后需要用sdedit将其转换为.png文件,方能上传到Confluence上贴到设计文档里,我希望.png文件跟.sd文件有相同的basename,那么用下面这个shell函数可以减轻一些重复输入的劳动力

1
2
3
4
5
# 根据.sd文件生成同名的.png文件
function sdpng() {
basename=${1}
/usr/local/bin/sdedit -t png -o ${basename}.png ${basename}.sd
}

只需要我输入一次文件名即可,效果如下
shell函数的效果

使用Alfred的Snippets功能

Alfred带有一个叫做Snippets的特性
Alfred的Snippets特性
它跟上面所说的alias很相似,但它不是由shell自己处理悄悄展开的,它是显式地输入一长串的字符。比如说我定义了三个短语:gpd、gct,以及gpt,它们分别会展开为

1
2
3
git push -u origin develop
git checkout test
git push -u origin test

效果如下
Alfred的Snippets自动展开的效果
Alfred的Snippets也跟alias一样是不能接受参数的,不过支持一些占位符,可以展开为一些特定模式的动态内容。一个比较有用的是{cursor}这个占位符,可以让光标定位至此。例如我可以定义这样的一串展开结果

1
SELECT * FROM `user_info` WHERE `userId` = {cursor}\G

这样我敲入对应的短语后就可以正确定位到WHERE语句,然后直接输入要查询的参数即可,效果如下
cursor占位符的效果

除了Alfred之外,还有其它的通过snippet提高输入效率的软件,比如aTextDash,不过我没有实际地用过,就不多说了。

后记

没有代码才是最快地输入代码的方式