Deploy org agenda files on save

Table of Contents

1. Issued

我過往同步 org agenda files 方法:

  1. Github repo 用 git push/pull 去同步我的 agenda files。但是在多台電腦切換就會變得麻煩,而且不夠靈活。
  2. 把 org 文件直接放在移動硬盤裡。最大問題是會把硬盤遺留在某個機子上。

最後我選擇了直接寫 emacs lisp 吧!(雖然不太會寫…)但經過 Google search 找到幾個 function 發現能做到想要的功能 - 在用 Emacs 保存指定的 org files 時進行 sync。這裡的 sync 是把自己的 agenda view 上傳到服務器上, 這樣當我隨時隨地用瀏覽器打 https://some-domain.com/agenda 就可以查看到我的 agenda 啦!

2. org-mode-hook & after-save-hook

我發現 Emacs 方便地方在於它的很多 packages 或者 mode (php-mode, web-mode, python-mode,etc…) 它都會帶一個 hook function。 在 Emacs 啟動某個 mode 的時候就會觸法它的 hook 。 這個設計在很多系統, Web Framework 等,都有它的身影。 比如 Ruby on Rails:

在 User record 創建之前 call MaybeAddName

class User < ApplicationRecord
  before_create MaybeAddName
end

class MaybeAddName
  def self.before_create(record)
    if record.name.blank?
      record.name = record.login.capitalize
    end
  end
end

有了這個 org-mode-hook 就可以把我們想運行的代碼用這個 hook 掛到 org mode上, 當 org mode 載入 emacs buffer 時就會運行:

(add-hook 'org-mode-hook 'my/upload-agenda-after-save-hook)

第一個條件達成,接下來就是當 org 文件用 Emacs 儲存的時候觸發上傳 agenda view 功能, 好在 Emacs 優秀的 api 設計,在 Emacs save 一個文件的時候就會觸發 after-save-hook, 於是:

(defun my/upload-agenda-after-save-hook ()
     (add-hook 'after-save-hook 'my/agenda-deploy))

這樣需要的鉤子就完成了,我們的 my/agenda-deploy 只會在 org mode 加上儲存 org 文件時才觸發。

3. s-prefix? & buffer-file-name

接下來問題是,我們不需要所有 org files 在儲存的時候都上傳,我們只需要指定特定幾個文件來做就可以了。

首先這是我目前的 org 目錄。

org
├── deploy.sh
├── gtd
│   ├── agenda.org
│   ├── done.org
│   ├── inbox.org
│   ├── project.org
│   └── read.org

實際上我的 agenda files 都在 ~/org/gtd/ 里面,所以最簡單就是直接檢查當前 Org files 的文件目錄是不是 under org/gtd 目錄裡面。

直接運行這個 command 就可以獲得當前正在編輯的文件名加上 absolute path。

(buffer-file-name)

有了這個 absolute path 我們就可以知道它是不是 under 在 org/gtd裡

(s-prefix? (expand-file-name "~/org/gtd/") (buffer-file-name (current-buffer)))

expand-file-name 是方便 function, 获得指定目录或者文件的 absolute path。

(expand-file-name "~/org/gtd/")

s-prefix? 它是 s-starts-with? 的 alias, 同一个function。 而 s-starts-with? 在 Doc 里面一句簡單易懂的解釋:

Does S start with PREFIX?

這個解釋直接就是我們想要的。

這是一個條件判斷,我們把 s-prefix? 放在 when 裡面:

(when (s-prefix? (expand-file-name "~/org/gtd/") (buffer-file-name (current-buffer)))
  (
   ;; deploy action here....
                           )
)

4. Export Agenda view

在 deploy agenda view 之前,要把我的 agenda view export 出來。

(add-to-list 'org-agenda-custom-commands
              '("g" "Get Things Done (GTD)"
                (
                 (tags "inbox+TODO=\"TODO\"+PRIORITY=\"A\"|project+TODO=\"TODO\"+PRIORITY=\"A\"|read+TODO=\"TODO\"+PRIORITY=\"A\""
                            (
                             (org-agenda-prefix-format "  %?-12t% s [%e] ")
                             (org-agenda-overriding-header "\nGet thing Done\n")))
                 ....

                nil
                ("~/org/index.html")))

這個是我目前自己用的 custom agenda view,你會看到在最後的 ~~/org/index.html~,便是 agenda 最終export 的目的地。

運行這個 command, emacs 就可以根據後綴去 export agenda view, 我這裡要上傳到 Web Server 所以後綴是 html。

(org-store-agenda-views)

5. Deploy Agenda html

上傳某文件到服務器的命令就是

scp -r ./index.html youruser@yourserver:/var/www/html/agenda/

我把它寫在了 deploy.sh 裡面。

6. start-process

最後一步就是用 Emacs 去 call 這個 deploy.sh

在 Emacs 運行系統指令:

(shell-command "ls")

但直接 (shell-command './deploy') 會讓 Emacs 卡住, 因為 scp 是要連結伺服器,上傳文件,斷開連結這些動作,簡單來說我們讓它背後自己跑就可以了,不影響emacs 進程。

我們需要用到 start-process

  • (start-process NAME BUFFER PROGRAM &rest PROGRAM-ARGS)
(let ((default-directory "~/org/"))
    (start-process "" nil "sh" "deploy.sh"))

default-directory 將 scope 裡面的目錄跳到指定目錄,這樣就省下打一長串的 path 了。

最後把所有東西加上

(defun my/agenda-deploy ()
  (when (s-prefix? (expand-file-name "~/org/gtd/") (buffer-file-name (current-buffer)))
    (let ((default-directory "~/org/"))
      (shell-command "rm index.html")
      (org-store-agenda-views)
      (start-process "" nil "sh" "deploy.sh"))
    ))

(defun my/upload-agenda-after-save-hook ()
     (add-hook 'after-save-hook 'my/agenda-deploy))

(add-hook 'org-mode-hook 'my/upload-agenda-after-save-hook)

7. References

Date: 2023-10-30 Mon 00:00

Author: Terry Fung

Created: 2024-11-10 Sun 14:09

Emacs 29.4 (Org mode 9.6.15)

Validate