拿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中用下面的代码获取

(nth 4 (org-heading-components))

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

拿Emacs对接我的cuckoo

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

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

(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库,示例代码如下

(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函数可以解析成代表着秒数的几个数字

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

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

(org-entry-get nil "SCHEDULED")

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

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

(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)发送给接口了。

全文完。

阅读原文

相关推荐