Sidebar infrastructure and widget framework for vulpea notes.
- Per-frame sidebar with configurable position and size
- Widget system built on vui components
- Default widgets: outline, backlinks, forward links, stats
- Easy API for creating custom widgets
- Auto-hide when switching to non-vulpea buffers
(package-install 'vulpea-ui)(straight-use-package 'vulpea-ui)(elpaca vulpea-ui)(require 'vulpea-ui)
;; Open sidebar
(vulpea-ui-sidebar-open)
;; Or toggle with a keybinding
(global-set-key (kbd "C-c v s") #'vulpea-ui-sidebar-toggle)To automatically open the sidebar when entering org-mode buffers:
(add-hook 'org-mode-hook #'vulpea-ui-sidebar-open);; Position: 'right (default), 'left, 'top, 'bottom
(setq vulpea-ui-sidebar-position 'right)
;; Size as fraction of frame (0.0-1.0)
(setq vulpea-ui-sidebar-size 0.33);; Maximum heading depth (nil = unlimited)
(setq vulpea-ui-outline-max-depth 3);; Show/hide content previews
(setq vulpea-ui-backlinks-show-preview t)
;; Characters before/after link in prose previews
(setq vulpea-ui-backlinks-prose-chars-before 30)
(setq vulpea-ui-backlinks-prose-chars-after 50)
;; Filter which notes appear in backlinks
;; (function receiving vulpea-note, return non-nil to include)
(setq vulpea-ui-backlinks-note-filter
(lambda (note)
(not (member "archive" (vulpea-note-tags note)))))
;; Filter by context type
;; t = all types, or a list of: meta, header, table, list, quote, code, footnote, prose
(setq vulpea-ui-backlinks-context-types t)
;; Sorting: nil (no sorting), 'title-asc, 'title-desc, or custom function
(setq vulpea-ui-backlinks-sort 'title-asc);; Auto-hide sidebar when switching to non-vulpea buffers
;; When nil, sidebar remains visible with stale content
(setq vulpea-ui-sidebar-auto-hide t)
;; Start widgets collapsed
(setq vulpea-ui-default-widget-collapsed nil)
;; Auto-refresh sidebar on save and idle (enabled by default)
(setq vulpea-ui-auto-refresh t)
;; Idle delay before auto-refresh (in seconds)
(setq vulpea-ui-auto-refresh-delay 1.5)For large org files or many backlinks, enable fast parsing:
;; Skip org-mode hooks during parsing (faster but may cause issues
;; if your setup depends on mode hooks for org-element parsing)
(setq vulpea-ui-fast-parse t)| Command | Description |
|---|---|
vulpea-ui-sidebar-open | Open the sidebar |
vulpea-ui-sidebar-close | Close the sidebar |
vulpea-ui-sidebar-toggle | Toggle sidebar visibility |
vulpea-ui-sidebar-refresh | Force refresh content |
| Key | Action |
|---|---|
q | Close sidebar |
g | Refresh content |
TAB | Navigate to next widget |
S-TAB | Navigate to previous widget |
RET | Activate widget at point |
TAB, S-TAB, and RET are inherited from vui-mode. q and g are sidebar-specific.
For quick navigation to any widget, consider ace-link-vui.
Shows character count, word count, and link count for the current note.
Displays the heading structure of the note. Click headings to navigate.
Shows notes that link to the current note, grouped by file with:
- Heading path context (where in the document the link appears)
- Content preview with context type indicators:
⊢meta property§header▤table·list item>quoteλcode†footnote- (no indicator) prose
Shows notes that the current note links to.
Widgets are registered with vulpea-ui-register-widget, which allows filtering by predicate and ordering.
vulpea-ui registers these widgets by default:
| Widget | Order | Component |
|---|---|---|
stats | 100 | vulpea-ui-widget-stats |
outline | 200 | vulpea-ui-widget-outline |
backlinks | 300 | vulpea-ui-widget-backlinks |
links | 400 | vulpea-ui-widget-links |
(vulpea-ui-register-widget 'my-widget
:component 'my-custom-widget-component
:predicate #'my-note-predicate ; optional: only show when this returns non-nil
:order 150) ; optional: default 100vulpea-ui-widget-set updates a single property on an already-registered widget. This works for any widget, including the built-in ones, so you can customise them without redefining anything.
;; Change a widget's order
(vulpea-ui-widget-set 'stats :order 50)
;; Attach a predicate to a built-in widget
(vulpea-ui-widget-set 'outline :predicate #'my/show-outline-p)
;; Remove a widget
(vulpea-ui-unregister-widget 'links)Built-in widgets have no predicate by default, so they are shown for every note. You can install one with vulpea-ui-widget-set to decide per note whether the widget shows up. A common recipe is a global default variable combined with an org property that overrides it on individual notes.
The example below hides the outline widget by default and shows it only on notes that set :OUTLINE: t. Flip my/vulpea-ui-always-show-outline to t to invert the default, in which case :OUTLINE: nil hides the widget on specific notes.
(defvar my/vulpea-ui-always-show-outline nil
"When non-nil, show the outline widget unless a note opts out.")
(defun my/vulpea-ui-show-outline-p (note)
"Return non-nil if the outline widget should be shown for NOTE.
An `OUTLINE' property on the note overrides the default variable."
(if-let* ((props (vulpea-note-properties note))
(entry (assoc "OUTLINE" props)))
(not (equal (cdr entry) "nil"))
my/vulpea-ui-always-show-outline))
(vulpea-ui-widget-set 'outline :predicate #'my/vulpea-ui-show-outline-p)The same pattern works for any widget (stats, backlinks, links, or your own).
- Widgets are filtered by their
:predicate(if any) against the current note - Remaining widgets are sorted by
:order(ascending) - Each widget’s
:componentis rendered
This allows packages like vulpea-journal to register context-specific widgets that only appear when viewing certain notes.
Create custom widgets using vui’s vui-defcomponent macro:
(vui-defcomponent my-custom-widget ()
"My custom widget."
:render
(let ((note (use-vulpea-ui-note)))
(vui-component 'vulpea-ui-widget
:title "My Widget"
:count 42
:children
(lambda ()
(vui-text "Custom content here")))))Register the widget:
(vulpea-ui-register-widget 'my-widget
:component 'my-custom-widget
:order 250) ; after outline, before backlinksFor context-specific widgets (e.g., only for notes with a certain tag):
(defun my-project-note-p (note)
"Return non-nil if NOTE is a project note."
(member "project" (vulpea-note-tags note)))
(vulpea-ui-register-widget 'project-tasks
:component 'my-project-tasks-widget
:predicate #'my-project-note-p
:order 150)Cleans org-mode markup from text for display purposes:
(vulpea-ui-clean-org-markup text)Transformations:
- Links:
[[url][title]]→title,[[url]]→url(bare[[id:...]]links are removed) - Drawers:
:PROPERTIES:...:END:blocks are removed - Metadata:
#+TITLE:,#+FILETAGS:, etc. lines are removed - Whitespace: multiple spaces/tabs collapsed to single space
Useful for rendering previews in custom widgets.
| Face | Description |
|---|---|
vulpea-ui-widget-header-face | Widget header text |
vulpea-ui-widget-count-face | Widget count numbers |
vulpea-ui-outline-heading-face | Outline headings |
vulpea-ui-stats-face | Statistics text |
vulpea-ui-backlink-preview-face | Backlink preview text |
vulpea-ui-backlink-heading-face | Backlink heading path |
vulpea-ui-backlink-meta-key-face | Meta block keys |
vulpea-ui-backlink-meta-value-face | Meta block values |
vulpea-ui-backlink-context-face | Context type indicators |
- vulpea-journal - Daily journaling with calendar, navigation, and “on this day” widgets
- ace-link-vui - Ace-link style navigation for VUI buffers
Copyright (C) 2024-2026 Boris Buliga
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
If you enjoy this project, you can support its development via GitHub Sponsors or Patreon.




