Org-roam(v2) 以及 org-roam-ui 的使用姿势(已支持Emacs 29 内置的 sqlite)

之光的xeft感觉不错啊,谢谢推荐,我试试看。我之前感觉org roam太重,加上自己还 hack了太多东西,一直想换个轻量级的,试用了zk等一堆包,都有一些局限性不够灵活还是没换掉

zk 的优缺点如何? org-super-links 貌似太简陋了,不成熟。

我感觉super-link更成熟一些,zk最大的优点是desk以及检索方便,但因为过度依赖动态的搜索,无法自动更新文件里面的链接,所以有时候反而误导用户以为当前的文件只有一个链接。我个人很喜欢zk这种轻量级的方法,最终放弃的另一个原因是zk对于org的融合有点割裂,虽然能用org-id作为链接,但zk的做法跟org-id没啥关系,我get-id后还是得手动处理。私以为zk其实是希望统一所有的文本格式都能处理,因此直接放弃了org mode的许多特性转而换用自己的方法

同时,zk这种纯搜索的方法在大量文件的情况下也不如roam或者xeft之类的数据库方法

谢谢分享zk使用经验。zk不能自动更新链接,且不能很好与orgmode融合的话,使用的意义就不是很大了。

org-super-link 我只看了简介。

目前我用org-roam的唯一理由就是双链,以及未来的可扩展性。它的缺点就是总是有点小毛病,虽然作者更新很快,但总给人一种不稳定的感觉。

另,能分享一下你的方法,或者代码吗?谢谢 :grinning:

关于org-roam的代码我大概分为4个部分

  1. 一些基础设置
  2. 对于标题链接的显示
  3. org-roam 和 org-agenda 的结合
  4. citar 文献操作的结合

其中2是从doom emacs 拿出来的, 3是org- roam论坛一位大佬的方法,4是org-roam作者分享他记录文献的流程,我从中学了不少我能用的方法

我很粗略的写了一点代码是干啥的,因为大部分都是我重组或者抄的,如果你感兴趣,建议去找找原代码出处,我有空可能会写一篇详细的工作流例如录一些动图来介绍这些方法。

1 个赞
  1. 首先是基础的设置,这里感谢论坛大佬的提醒,我也使用了built-in 的sqlite
     (setq org-roam-database-connector 'sqlite-builtin
     org-roam-mode-section-functions (list #'org-roam-backlinks-section
                                           #'org-roam-reflinks-section
                                           ;; #'org-roam-unlinked-references-section
                                           )
     org-roam-directory "~/Documents/emacs/orgmode/roam/"
     org-roam-dailies-directory "~/Documents/emacs/orgmode/roam"

     org-roam-db-gc-threshold most-positive-fixnum)

;; 使用侧边栏而不是完整buffer
      (add-to-list 'display-buffer-alist
                   '("\\*org-roam\\*"
                     (display-buffer-in-side-window)
                     (side . right)
                     (slot . 0)
                     (window-width . 0.25)
                     (window-parameters . ((no-other-window . t)
                                           (no-delete-other-windows . t)))))

  1. 标题链接,org-roam默认会把所有的标题链接和文件链接视为同一级,这样有时会因此重复索引或者不直观的特点,我从doom里面抄了一堆代码, 效果如第四个node那样(2021-0815是文件一级,而具体的标题一级是后面的链接)

代码


      ;; Codes blow are used to general a hierachy for title nodes that under a file
      (cl-defmethod org-roam-node-doom-filetitle ((node org-roam-node))
        "Return the value of \"#+title:\" (if any) from file that NODE resides in.
      If there's no file-level title in the file, return empty string."
        (or (if (= (org-roam-node-level node) 0)
                (org-roam-node-title node)
              (org-roam-get-keyword "TITLE" (org-roam-node-file node)))
            ""))
      (cl-defmethod org-roam-node-doom-hierarchy ((node org-roam-node))
        "Return hierarchy for NODE, constructed of its file title, OLP and direct title.
        If some elements are missing, they will be stripped out."
        (let ((title     (org-roam-node-title node))
              (olp       (org-roam-node-olp   node))
              (level     (org-roam-node-level node))
              (filetitle (org-roam-node-doom-filetitle node))
              (separator (propertize " > " 'face 'shadow)))
          (cl-case level
            ;; node is a top-level file
            (0 filetitle)
            ;; node is a level 1 heading
            (1 (concat (propertize filetitle 'face '(shadow italic))
                       separator title))
            ;; node is a heading with an arbitrary outline path
            (t (concat (propertize filetitle 'face '(shadow italic))
                       separator (propertize (string-join olp " > ") 'face '(shadow italic))
                       separator title)))))

     (setq org-roam-node-display-template (concat "${type:15} ${doom-hierarchy:80} " (propertize "${tags:*}" 'face 'org-tag)))

6 个赞
  1. org-roam agenda结合 这些代码的效果是,会对所有有TODO的文件动态添加到agenda列表。因为是用了org-roam的数据库,所以速度很快。我没细看代码,大概的原理是,当用户添加TODO的时候,会自动给文件打一个project的tag,如果改文件所有TODO都没有了,则益处project的tag,然后会用数据库读取包含project tag的文件添加到agenda文件列表

如果你打算使用这个功能,建议去关注org-roam论坛中该代码的作者,他在长期维护(从org-roam v1 一直维护到现在的v2)

    (defun vulpea-project-p ()
    "Return non-nil if current buffer has any todo entry.
  TODO entries marked as done are ignored, meaning the this
  function returns nil if current buffer contains only completed
  tasks."
    (seq-find                                 ; (3)
     (lambda (type)
       (eq type 'todo))
     (org-element-map                         ; (2)
         (org-element-parse-buffer 'headline) ; (1)
         'headline
       (lambda (h)
         (org-element-property :todo-type h)))))

  (defun vulpea-project-update-tag ()
      "Update PROJECT tag in the current buffer."
      (when (and (not (active-minibuffer-window))
                 (vulpea-buffer-p))
        (save-excursion
          (goto-char (point-min))
          (let* ((tags (vulpea-buffer-tags-get))
                 (original-tags tags))
            (if (vulpea-project-p)
                (setq tags (cons "project" tags))
              (setq tags (remove "project" tags)))

            ;; cleanup duplicates
            (setq tags (seq-uniq tags))

            ;; update tags if changed
            (when (or (seq-difference tags original-tags)
                      (seq-difference original-tags tags))
              (apply #'vulpea-buffer-tags-set tags))))))

  (defun vulpea-buffer-p ()
    "Return non-nil if the currently visited buffer is a note."
    (and buffer-file-name
         (string-prefix-p
          (expand-file-name (file-name-as-directory org-roam-directory))
          (file-name-directory buffer-file-name))))

  (defun vulpea-project-files ()
      "Return a list of note files containing 'project' tag." ;
      (seq-uniq
       (seq-map
        #'car
        (org-roam-db-query
         [:select [nodes:file]
          :from tags
          :left-join nodes
          :on (= tags:node-id nodes:id)
          :where (like tag (quote "%\"project\"%"))]))))

  (defun vulpea-agenda-files-update (&rest _)
    "Update the value of `org-agenda-files'."
    (setq org-agenda-files (vulpea-project-files)))

  (add-hook 'find-file-hook #'vulpea-project-update-tag)
  (add-hook 'before-save-hook #'vulpea-project-update-tag)

  (advice-add 'org-agenda :before #'vulpea-agenda-files-update)
  (advice-add 'org-todo-list :before #'vulpea-agenda-files-update)

  ;; functions borrowed from `vulpea' library
  ;; https://github.com/d12frosted/vulpea/blob/6a735c34f1f64e1f70da77989e9ce8da7864e5ff/vulpea-buffer.el

  (defun vulpea-buffer-tags-get ()
    "Return filetags value in current buffer."
    (vulpea-buffer-prop-get-list "filetags" "[ :]"))

  (defun vulpea-buffer-tags-set (&rest tags)
    "Set TAGS in current buffer.
  If filetags value is already set, replace it."
    (if tags
        (vulpea-buffer-prop-set
         "filetags" (concat ":" (string-join tags ":") ":"))
      (vulpea-buffer-prop-remove "filetags")))

  (defun vulpea-buffer-tags-add (tag)
    "Add a TAG to filetags in current buffer."
    (let* ((tags (vulpea-buffer-tags-get))
           (tags (append tags (list tag))))
      (apply #'vulpea-buffer-tags-set tags)))

  (defun vulpea-buffer-tags-remove (tag)
    "Remove a TAG from filetags in current buffer."
    (let* ((tags (vulpea-buffer-tags-get))l
           (tags (delete tag tags)))
      (apply #'vulpea-buffer-tags-set tags)))

  (defun vulpea-buffer-prop-set (name value)
    "Set a file property called NAME to VALUE in buffer file.
  If the property is already set, replace its value."
    (setq name (downcase name))
    (org-with-point-at 1
      (let ((case-fold-search t))
        (if (re-search-forward (concat "^#\\+" name ":\\(.*\\)")
                               (point-max) t)
            (replace-match (concat "#+" name ": " value) 'fixedcase)
          (while (and (not (eobp))
                      (looking-at "^[#:]"))
            (if (save-excursion (end-of-line) (eobp))
                (progn
                  (end-of-line)
                  (insert "\n"))
              (forward-line)
              (beginning-of-line)))
          (insert "#+" name ": " value "\n")))))

  (defun vulpea-buffer-prop-set-list (name values &optional separators)
    "Set a file property called NAME to VALUES in current buffer.
  VALUES are quoted and combined into single string using
  `combine-and-quote-strings'.
  If SEPARATORS is non-nil, it should be a regular expression
  matching text that separates, but is not part of, the substrings.
  If nil it defaults to `split-string-default-separators', normally
  \"[ \f\t\n\r\v]+\", and OMIT-NULLS is forced to t.
  If the property is already set, replace its value."
    (vulpea-buffer-prop-set
     name (combine-and-quote-strings values separators)))

  (defun vulpea-buffer-prop-get (name)
    "Get a buffer property called NAME as a string."
    (org-with-point-at 1
      (when (re-search-forward (concat "^#\\+" name ": \\(.*\\)")
                               (point-max) t)
        (buffer-substring-no-properties
         (match-beginning 1)
         (match-end 1)))))

  (defun vulpea-buffer-prop-get-list (name &optional separators)
    "Get a buffer property NAME as a list using SEPARATORS.
  If SEPARATORS is non-nil, it should be a regular expression
  matching text that separates, but is not part of, the substrings.
  If nil it defaults to `split-string-default-separators', normally
  \"[ \f\t\n\r\v]+\", and OMIT-NULLS is forced to t."
    (let ((value (vulpea-buffer-prop-get name)))
      (when (and value (not (string-empty-p value)))
        (split-string-and-unquote value separators))))

  (defun vulpea-buffer-prop-remove (name)
    "Remove a buffer property called NAME."
    (org-with-point-at 1
      (when (re-search-forward (concat "\\(^#\\+" name ":.*\n?\\)")
                               (point-max) t)
        (replace-match ""))))
  1. 对于 citar文献的融合

鉴于这里是一种方法而不是形式,我建议你搜一下 org-roam作者写的那一篇关于他如何记录笔记和科研的方法,理解思路然后再阅读代码

首先是 citar部分,可以从bib文件一键生成node和索引,包括文献名和文献文件的链接。

  (setup citar
    (:option org-cite-global-bibliography '("~/Documents/emacs/orgmode/bibliography/better_zotero_bib.bib")
             org-cite-insert-processor 'citar
             org-cite-follow-processor 'citar
             org-cite-activate-processor 'citar
             citar-bibliography org-cite-global-bibliography))

  ;; borrowed from https://jethrokuan.github.io/org-roam-guide/ as a method for insert notes for reference
  (defun lewis/org-roam-node-from-cite (keys-entries)
    (interactive (list (citar-select-ref :multiple nil :rebuild-cache t)))
    (let ((title (citar--format-entry-no-widths (cdr keys-entries)
                                                "${author editor}::${title}")))
      (org-roam-capture- :templates
                         '(("r" "reference" plain "%?" :if-new
                            (file+head "reference/${citekey}.org"
                                       ":PROPERTIES:
  :ROAM_REFS: [cite:@${citekey}]
  :END:
  ,#+title: ${title}\n* Action notes\n* Idea notes\n* Sealed notes")
                            :unnarrowed t))
                         :info (list :citekey (car keys-entries))
                         :node (org-roam-node-create :title title)
                         :props '(:finalize find-file))))

然后是添加了新的一列,你可以简单理解为独立的一组tag,这组tag是根据文件路径显示的

      (cl-defmethod org-roam-node-type ((node org-roam-node))
        "Return the TYPE of NODE."
        (condition-case nil
            (file-name-nondirectory
             (directory-file-name
              (file-name-directory
               (file-relative-name (org-roam-node-file node) org-roam-directory))))
          (error "")))

效果就是前面的reference或者article分类。

上面的代码可能看起来有点乱,我今晚也在忙别的写的仓促。你可以在我的配置里 GitHub - nowislewis/nowisemacs: A full-blown emacs configuration framework with easy abstraction 找到更为系统的组织形式

我是用org-mode管理所有的配置,都在里面的一个 init.org里。说实话因为转了borg管理包,你不一定能在init.org看到所有的包,有疑问欢迎提出,我入门的时受过很多人的帮助,也很乐意分享任何我能有的一点知识

8 个赞

非常感谢!

重新定义 org-roam-node 的展示很有用,我在 org-roam 作者的配置里面也找到了,很好。

与org-agenda 结合的好处还是没看得太明白。我是直接把一个 task.org 丢到 roam 文件夹里面。

org-roam 作者 Jethro 关于记笔记的方法确实很值得一看,我也从中学习了很多方法。

哈哈,我也是从别人那里学到的。agenda 感兴趣的话可以看这篇博客:Boris Buliga - Task management with org-roam Vol. 5: Dynamic and fast agenda

好处大概就是,我在任何一个node里都可以添加TODO,比如某一个node是读书笔记,书没读完,或者是工作进行一半,或者是笔记写一半等等,这些node都会被动态的添加到agenda里,如果TODO完成了,则会自动移除。如果把所有的node都添加到agenda里,速度会非常慢,而这种动态的方法几乎没有速度影响

1 个赞

好的。感谢!明白那段代码的意思了。

配置(cl-defmethod org-roam-node-type ((node org-roam-node))之后,启动一直出现 error: Unknown specializer org-roam-node,这种情况怎么解决哇?看网上说 ;; This needs to be after the org-roam’ declaration as it is dependent on the structures of org-roam’. 但是不太懂什么意思

原因可能是需要在load org roam之后再执行这段代码,加个with eval after load 试试

十分感谢,我用这个成功了。还有一个问题想请教一下,我现在用的是 org-roam v2, 有没有什么比较方便的预览org的方式啊

consult-org-roam可以做到在浏览node列表的时候同步预览org文件的内容,我一般用这个,非常方便

3 个赞

可以分享一下你的配置和使用 consult-org-roam 的 workflow 吗,我按照官方给的配置都不太有效,比如 consult-org-roam-search 永远都是 no matched

(use-package consult-org-roam
   :ensure t
   :init
   (require 'consult-org-roam)
   ;; Activate the minor-mode
   (consult-org-roam-mode 1)
   :custom
   (consult-org-roam-grep-func #'consult-ripgrep)
   :config
   ;; Eventually suppress previewing for certain functions
   (consult-customize
    consult-org-roam-forward-links
    :preview-key (kbd "M-."))
   :bind
   ("C-c n e" . consult-org-roam-file-find)
   ("C-c n b" . consult-org-roam-backlinks)
   ("C-c n r" . consult-org-roam-search))
1 个赞

我的配置就一行,简单的打开consult-org-roam-mode,它就会自动激活预览,别的也没配置什么……

请问这句话的含义是 Emacs 29 已经不需要依赖系统的 sqlite 了吗?

我在安装 emacsql-sqlite-builtin 发现它似乎找不到内置的 sqlite, 依然需要手动安装. 比如:

(use-package emacsql-sqlite-builtin
  :init
  (require 'emacsql)
  (require 'emacsql-sqlite))

;; Error (use-package): emacsql-sqlite-builtin/:init: Cannot open load file: No such file or directory, emacsql

依然需要手动安装 emacs-sqlemacsql-sqlite 二者, 如

(use-package emacsql-sqlite-builtin
  :init
  (use-package emacsql)
  (use-package emacsql-sqlite))

就没有任何报错. 但这样似乎显得多余.

P.S.

emacs-version:

GNU Emacs 29.0.50 (build 1, aarch64-apple-darwin22.1.0, NS appkit-2282.14 Version 13.0 (Build 22A5342f)) of 2022-09-20

OS: macOS; 安装方式:

brew install emacs-plus@29 --with-native-comp

我的理解是 Emacs 29 内置了对 sqlite3的绑定,不需要通过动态模块来实现对 sqlite3的支持。 你系统中还是要安装 sqlite3 这个程序的。

按理说你只要安装好 emacsql-sqlite-builtin以及他的相关依赖就可以了,你是用过什么方式安装包?(比如:package.el, straight.el, Borg)

用 package.el 通过 melpa 安装的话,这样应该就可以了:

(use-package emacsql-sqlite-builtin :ensure t)
1 个赞

使用的是 straight.el. 非常感谢, 这个提醒了我, 目前问题已经解决. 原因大致是 emacsql 和 emacsql-sqlite 这两个包应该隶属于 org-roam, 而按照 straight.el 的逻辑, 这两个包在加载 org-roam 前都不能被调用. 于是把 (use-package emacsql-sqlite-builtin) 这一行放在 (use-package org-roam) 后面加载就没有出错了.

在 emacs 29 刚安装了 emacsql-sqlite-builtin 然后设置 (setq org-roam-database-connector 'sqlite-builtin) ,load org-roam 报错如下

Lisp error: (invalid-slot-type emacsql-sqlite-builtin-connection process process #<sqlite db=0x... name=/path/to/roam-db.db>)

单独使用 (emacsql-sqlite-builtin "/path/to/roam-db.db") 试了一下报同样的错误。emacsql emacsql-sqlite emacsql-sqlite-builtin都安装的melpa最新的版本。

网上搜了一下没有搜到,有碰到同样问题的吗?