Skip to content
Projects

jonathanchu/grove has 77 GitHub stars and 5 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-15 08:52 UTC. Last push 2026-09-02.

How these counts are measured

Measured star growth

in 24h
0
in 7 days
Collecting history
in 15 days
Collecting history
in 1 month
Collecting history

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

About

An Obsidian-like note-taking mode for Emacs

Productivity

README.md

Original

#+title: grove.el #+author: Jonathan Chu

#+html:

An Obsidian-like note-taking mode for Emacs. One keybinding opens a full workspace with a file tree sidebar and your notes.

[[file:grove-screenshot.png]]

[[https://melpa.org/#/grove][https://melpa.org/packages/grove-badge.svg]] [[https://img.shields.io/badge/license-GPL--3.0-blue.svg]]

  • Features
  • Org and Markdown — both formats in one vault, read side by side
  • File tree sidebar with expand/collapse, indent guides, current file tracking, item counts, optional nerd font icons, and note preview on navigation
  • Quick capture — just type, first line becomes the title, saved to your inbox
  • Wikilinks — =[[note title]]= syntax with font-lock, click to follow, create on missing
  • Backlinks — ripgrep-powered, computed on demand, displayed in a side panel
  • Daily notes — one keybinding for today, yesterday, or tomorrow
  • Search — full-text ripgrep search with optional Consult integration
  • Tag search — find notes by =#hashtag= or org =:tag:= syntax
  • Inbox review — triage untagged notes
  • Graph view — visualize note connections via Graphviz
  • Profiles — keep separate vaults for personal and work notes, switch with one keybinding

No database. No external Emacs dependencies. Just plain text files, a directory, and ripgrep.

  • Requirements

Optional:

  • Installation

** MELPA

Grove is available on [[https://melpa.org/#/grove][MELPA]]. After [[https://melpa.org/#/getting-started][adding MELPA]] to your package archives:

#+begin_src emacs-lisp (use-package grove :ensure t :bind-keymap ("C-c v" . grove-command-map) :custom (grove-directory "~/notes/") :config (global-grove-mode 1)) #+end_src

=global-grove-mode= activates =grove-mode= in any note that lives inside =grove-directory=, which enables wikilink font-locking and the =C-c C-l= keybinding for inserting links. Omit it if you'd rather enable =grove-mode= manually per buffer.

** Manual

Clone the repository and add it to your load path:

#+begin_src emacs-lisp (use-package grove :load-path "/path/to/grove" :bind-keymap ("C-c v" . grove-command-map) :custom (grove-directory "/notes/") :config (global-grove-mode 1)) #+end_src

  • Usage

Set your vault directory and press =C-c v v= to open the workspace.

| Key | Command | Description | |-----------+------------------------+--------------------------| | =C-c v v= | =grove-open= | Open the grove workspace | | =C-c v q= | =grove-close= | Close and restore layout | | =C-c v n= | =grove-capture= | Quick capture a new note | | =C-c v f= | =grove-find= | Find note by title | | =C-c v s= | =grove-search= | Full-text ripgrep search | | =C-c v d= | =grove-daily= | Open today's daily note | | =C-c v b= | =grove-backlinks= | Show backlinks for note | | =C-c v t= | =grove-search-tag= | Search by tag | | =C-c v i= | =grove-inbox-review= | Triage untagged notes | | =C-c v l= | =grove-link-insert= | Insert a wikilink | | =C-c v g= | =grove-graph= | Show vault graph | | =C-c v p= | =grove-switch-profile= | Switch the active vault |

** Tree sidebar

| Key | Action | |-------------+-------------------------------| | =RET= | Open file / toggle dir | | =TAB= | Toggle directory expand | | =n= / =C-n= | Next entry (with preview) | | =p= / =C-p= | Previous entry (with preview) | | =g= | Refresh tree | | =q= | Close sidebar |

** Capture

=C-c v n= opens =grove-capture=, a blank buffer in =grove-default-format=. Type freely — no prompts, no template. The first line becomes the title; everything below becomes the body.

  • =C-c C-c= saves to the inbox
  • =C-c C-k= discards

For example, typing:

#+begin_src org Project ideas for next quarter

  • migrate to ripgrep
  • add a graph view

#+end_src

and pressing =C-c C-c= writes =inbox/project-ideas-for-next-quarter.org=:

#+begin_src org ,#+title: Project ideas for next quarter

  • migrate to ripgrep
  • add a graph view

#+end_src

Filenames are derived from the title (lowercased, spaces to dashes). A numeric suffix is appended on collision.

** Org and Markdown

Grove reads =.org= and =.md= notes from the same vault at the same time. Point it at an existing Obsidian vault and the tree, search, backlinks, and graph work immediately — =[[wikilinks]]= are the same syntax in both formats, so nothing needs converting.

Titles and tags are read per format:

| | Org | Markdown | |-------+----------------------------------+-------------------------------------------------------| | Title | =#+title:= | front matter =title:=, first =#= heading, or filename | | Tags | =#+filetags:=, inline =#hashtag= | front matter =tags:=, inline =#hashtag= |

Front matter =tags:= accepts both YAML forms Obsidian writes — =tags: [a, b]= on one line, or a =- a= list beneath the key.

New notes — capture, daily notes, and the note offered when you follow a wikilink to a title that doesn't exist yet — are written in =grove-default-format=, which defaults to ='org=:

#+begin_src emacs-lisp (setq grove-default-format 'md) #+end_src

Existing notes are always read in their own format, whatever this is set to. Markdown notes grove creates get YAML front matter rather than a bare heading, so that a title punctuation strips out of the filename — =Project: Alpha= becomes =project-alpha.md= — still round trips.

The tree sidebar follows the same setting: notes in =grove-default-format= show as bare names, and notes in any other format keep their extension, so =meeting.org= and =meeting.md= in one directory stay tellable apart.

#+begin_example meeting meeting.md notes #+end_example

To restrict grove to one format, set =grove-file-extensions=:

#+begin_src emacs-lisp (setq grove-file-extensions '("org")) ; ignore .md files entirely #+end_src

=markdown-mode= is not required. Grove does not depend on it, and wikilinks work in =.md= notes whether or not it is installed — though you will probably want it for editing.

** Inbox review

=C-c v i= opens =grove-inbox=, a triage buffer that lists notes needing attention. The buffer groups notes by the reason they showed up — currently, Untagged: notes with no =#+filetags:= line and no inline =#hashtags=.

| Key | Action | |-------------+------------------------------| | =RET= | Visit the note at point | | =n= / =p= | Move to next / previous line | | =g= | Refresh the buffer | | =q= | Close the buffer |

The intended workflow is short loops: open the inbox, jump into a note with =RET=, add tags or wikilinks, save, and either come back with =C-c v i= or refresh in place with =g=. Notes drop off the list as you tag them, so the buffer doubles as a progress indicator.

The list is built from the vault cache, so it covers your whole vault — not just files under =grove-inbox-directory=. Tag a note anywhere and it disappears from the next refresh.

** Graph

=C-c v g= renders a graph of all notes and their =[[wikilinks]]= using Graphviz. Requires =dot= on your PATH.

The graph display adapts to your frame width — on wide frames (160+ columns) it opens as a right side panel, otherwise it uses a full buffer. You can override this with =grove-graph-display=: ='side=, ='buffer=, or ='auto= (default).

Use =+= / =-= to zoom in/out and =0= to fit to window.

** Profiles

A profile is a named vault. Set =grove-profiles= to keep separate note collections — a personal one and a work one, say — and switch between them with =C-c v p=.

#+begin_src emacs-lisp (setq grove-profiles '((personal :directory "/notes") (work :directory "/work/notes"))) #+end_src

Everything is scoped to the active profile. The tree sidebar, search, tag search, backlinks, the graph, inbox review, capture, and daily notes all operate on that vault alone, and each profile keeps its own note cache — so switching is instant and vaults never bleed into each other. The sidebar header shows which one you're in as =Grove [work]=.

A few things worth knowing:

  • With =grove-profiles= set, the first grove command prompts you to pick a profile, and =grove-directory= is ignored from then on. Leave =grove-directory= unset if you're using profiles.

  • The active profile isn't persisted. Emacs starts with none selected, so you'll be prompted again on the first command of each session.

  • =global-grove-mode= only enables =grove-mode= in the active profile's files. Notes in your other vaults open as plain text until you switch to their profile.

  • Grove points you at the right vault rather than doing the wrong thing quietly. Following a =[[wikilink]]= whose target lives elsewhere reports where it is instead of creating a duplicate, and =C-c v b= in a note from another profile tells you to switch first:

    #+begin_example Note "Recipes" is in profile [personal] — switch profiles to follow Note is in profile [work] — switch profiles first #+end_example

  • Configuration

#+begin_src emacs-lisp ;; Set your vault directory — required unless you use profiles below (setq grove-directory "~/notes/")

;; Optional: separate vaults, switched with C-c v p. Supersedes grove-directory. (setq grove-profiles '((personal :directory "/remoteFolder/personal") (work :directory "/localFolder/work") (other :directory "~/otherNotes")))

;; Optional: customize subdirectories (defaults shown) (setq grove-inbox-directory "inbox") (setq grove-daily-directory "daily")

;; Optional: which extensions count as notes (default shown) (setq grove-file-extensions '("org" "md"))

;; Optional: format for notes grove creates (default shown) (setq grove-default-format 'org) ; or 'md

;; Optional: daily note filename format (default shown) (setq grove-daily-format "%Y-%m-%d")

;; Optional: tree sidebar width (default shown) (setq grove-tree-width 30)

;; Optional: show nerd font icons in the tree sidebar (setq grove-tree-icons t)

;; Optional: graph view settings (defaults shown) (setq grove-graph-layout "neato") ; or "dot", "fdp", "sfdp" (setq grove-graph-display 'auto) ; or 'side, 'buffer (setq grove-graph-min-width 160) ; frame width threshold for auto #+end_src

  • License

GPL-3.0. See [[file:LICENSE][LICENSE]].