[分享] 不打断工作流的通用capture功能实现

当我们在emacs中进行一项日常工作,一个灵感跑到脑子里。这时,我们通常的做法是:打开自己用来记录灵感的文件,记录想法,切回正在编辑文件,继续当前工作… 尽管在emacs中我们有许多的方法快速打开那个“灵感文件”,比如设置bookmark等,但这个过程仍然容易打断我们的工作流。每一次的buffer切换,光标移动都意味着我们需要为这次临时的记录,多一次选择,多付出一些精力。

这让我开始思考,如何在保留当前编辑区域的情况下,快速记录临时的想法呢。事实上,org-capture 的思路是不错的。但是 org-capture 和 org-mode 绑定,我们需要一个更加通用的 capture 功能,支持任何格式,任意方式的记录。于是便有了 my-capture。

demo

代码 和 配置

代码
(defvar my-capture-return-winconf nil)
(defvar my-capture-buffer "*My Capture*")
(defvar my-capture-target-alist nil)
(defvar my-capture-target-key nil)

(defvar my-capture-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-s") #'my-capture-save)
    (define-key map (kbd "C-c C-c") #'my-capture-finalize)
    (define-key map (kbd "C-c C-k") #'my-capture-cancel)
    map))

(defun my-capture--parse-target (target-info)
  (pcase target-info
    ((pred stringp) (cons target-info 'append))
    ((pred consp)
     (cond
      ((functionp (car target-info)) (apply (car target-info) (cdr target-info)))
      ((stringp (car target-info)) target-info)))))

(defun my-capture--write ()
  (let* ((target-key my-capture-target-key)
         (content (buffer-substring (point-min) (point-max)))
         (target-info-lst (cdr (assoc target-key my-capture-target-alist))))
    (mapcar (lambda (target)
              (let* ((file-pos-cons (my-capture--parse-target target))
                     (file (car file-pos-cons))
                     (point-or-symbol (cdr file-pos-cons))
                     point)
                (with-current-buffer (find-file-noselect file)
                  (save-excursion
                    (pcase point-or-symbol
                      ('prepend (setq point (point-min)))
                      ('append (setq point (point-max)))
                      (_ (setq point point-or-symbol)))
                    (goto-char (or point (point-max)))
                    (insert content)))))
            target-info-lst)))

(defun my-capture-save ()
  "Save content to file and continue capturing."
  (interactive)
  (my-capture--write)
  (erase-buffer))

(defun my-capture-finalize ()
  "Finalize current capture"
  (interactive)
  (my-capture--write)
  (set-window-configuration my-capture-return-winconf)
  (kill-buffer my-capture-buffer))

(defun my-capture-cancel ()
  "Cancel current capture."
  (interactive)
  (set-window-configuration my-capture-return-winconf)
  (kill-buffer my-capture-buffer))

(define-minor-mode my-capture-mode
  "Minor mode for quick capture."
  :keymap my-capture-mode-map
  (if my-capture-mode
      (setq-local header-line-format
                  (substitute-command-keys
                   (format "\\<my-capture-mode-map>Capture %s, save `\\[my-capture-save]', finish \
`\\[my-capture-finalize]', cancel `\\[my-capture-cancel]'." my-capture-target-key)))
    (setq-local header-line-format nil)))

(defun my-capture ()
  "Pop up a side window to capture text."
  (interactive)
  (let ((keyword (completing-read "Choose a capture type: " my-capture-target-alist nil t)))
    (setq my-capture-return-winconf (current-window-configuration))
    (display-buffer-in-side-window (get-buffer-create my-capture-buffer) nil)
    (select-window (get-buffer-window my-capture-buffer))
    (erase-buffer)
    (setq-local my-capture-target-key keyword)
    (my-capture-mode 1)))
配置示例
(setq my-capture-target-alist
      '(("fleeting-note" (my-capture-note "Fleeting notes"))
        ("permanent-note" (my-capture-note "Permanent notes"))
        ("literature-note" (my-capture-note "Literature notes"))
        ("project-note" (my-capture-note "Project notes"))
        ("misc-note" (my-capture-note))
        ("todo" (my-capture-todo))
        ("inbox" (my-capture-inbox))))

(defvar my-capture-note-file "~/PARA/RESOURCE/Emacs/pkgs/capture/zknote.org")
(defvar my-capture-todo-file "~/PARA/RESOURCE/Emacs/pkgs/capture/zktodo.org")
(defvar my-capture-inbox-file "~/PARA/RESOURCE/Emacs/pkgs/capture/zkinbox.org")

(defun my-capture-note (&optional type)
  "TYPE should be one of Fleeting notes, Permanent notes,
Literature notes, Project notes."
  (let ((file my-capture-note-file)
        (timestamp (format-time-string "** %Y-%m-%d %H:%M:%S"))
        point)
    (with-current-buffer (find-file-noselect file)
      (save-excursion
        (goto-char (point-min))
        (if type
            (when (re-search-forward type nil t)
              (end-of-line)
              (insert "\n" timestamp "\n")
              (setq point (point)))
          (when (re-search-forward "Misc" nil t)
            (end-of-line)
            (insert "\n" timestamp "\n")
            (setq point (point))))))
    (cons file point)))

(defun my-capture-todo (&optional month-num)
  (let ((file my-capture-todo-file)
        (month (if month-num
                   (concat (format-time-string "%Y-")
                           (string-pad month-num 2 ?0 t))
                 (format-time-string "%Y-%m")))
        point)
    (with-current-buffer (find-file-noselect file)
      (save-excursion
        (goto-char (point-min))
        (when (re-search-forward (concat "^* " month) nil t)
          (insert "\n** TODO ")
          (setq point (point)))))
    (cons file point)))

(defun my-capture-inbox ()
  (let ((file my-capture-inbox-file)
        point)
    (with-current-buffer (find-file-noselect file)
      (save-excursion
        (goto-char (point-min))
        (insert "\n")
        (setq point (point-min))))
    (cons file point)))

配置说明

使用 my-capture 只需要配置 my-capture-target-alist 变量。变量主要支持以下格式:

(setq my-capture-target-alist
      '(;; 指定一个文件,默认在文件最后写入capture的内容(2的简写形式)
        ("target key1" "path/to/file")
        ;; 指定一个文件,在文件最后写入capture的内容
        ("target key2" ("path/to/file" . append))
        ;; 指定一个文件,在文件开头写入capture的内容
        ("target key3" ("path/to/file" . prepend))
        ;; 指定一个自定义函数,函数需要返回一个 cons-cell: (文件路径 . 光标位置)
        ("target key4" (func args))))
4 个赞

没太理解与 Org-capture 的区别. org-capture 后还是可以跳转到之前的Buffer,不需要切Buffer,光标也可以保留在原位。

1 个赞

大佬只开一个frame全屏吗,我是专门开一个frame来记笔记想法,另一个frame写点代码,两边分开都不全屏,有明显区分,基本不冲突

貌似org-roam 也有这功能

在 Emacs 里记笔记还有人用 org-mode 以外的插件吗(

楼主不妨说说 org-capture 有哪些令你不满意的地方。