拿Emacs對接我的cuckoo
cuckoo是一個我自己開發的類似待辦事項的工具,執行在我本地的電腦上。它有如下兩個介面:
- 傳入一個UNIX Epoch時間戳建立提醒
- 傳入一個標題以及提醒的ID來建立任務
這樣一來,便能在設定的時刻呼叫 alerter
在螢幕右上角彈出提醒。
我喜歡用 Emacs 的 org-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條目上的時候,會返回許多資訊(參見下圖)
其中下標為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))))))
既然知道如何解析(同時還知道如何提取解析後的內容),那麼接下來便是要能夠獲取上述示例程式碼中的 s
。 s
來自於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後才發現,原來需要傳遞 :sync
為 t
才可以讓 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
)傳送給介面了。
全文完。