+title: vulpea-ui
+author: Boris Buliga
Sidebar infrastructure and widget framework for [[https://github.com/d12frosted/vulpea][vulpea]] notes.
+begin_html
- Features
- Per-frame sidebar with configurable position and size
- Widget system built on [[https://github.com/d12frosted/vui.el][vui]] components
- Default widgets: outline, backlinks, forward links, stats
- Easy API for creating custom widgets
- Auto-hide when switching to non-vulpea buffers
- Installation
** Dependencies
- Emacs 29.1+
- [[https://github.com/d12frosted/vulpea][vulpea]] (v2)
- [[https://github.com/d12frosted/vui.el][vui]]
** Using package.el (MELPA)
+begin_src emacs-lisp
(package-install 'vulpea-ui)
+end_src
** Using straight.el
+begin_src emacs-lisp
(straight-use-package 'vulpea-ui)
+end_src
** Using elpaca
+begin_src emacs-lisp
(elpaca vulpea-ui)
+end_src
- Usage
+begin_src emacs-lisp
(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)
+end_src
** Automatic sidebar
To automatically open the sidebar when entering org-mode buffers:
+begin_src emacs-lisp
(add-hook 'org-mode-hook #'vulpea-ui-sidebar-open)
+end_src
- Configuration
** Sidebar position and size
+begin_src emacs-lisp
;; 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)
+end_src
** Outline widget
+begin_src emacs-lisp
;; Maximum heading depth (nil = unlimited) (setq vulpea-ui-outline-max-depth 3)
+end_src
** Backlinks widget
+begin_src emacs-lisp
;; 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)
+end_src
** Behaviour options
+begin_src emacs-lisp
;; 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)
+end_src
** Performance
For large org files or many backlinks, enable fast parsing:
+begin_src emacs-lisp
;; 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)
+end_src
- Commands
| 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 |
- Sidebar keybindings
| 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 [[https://github.com/d12frosted/ace-link-vui][ace-link-vui]].
- Default widgets
** Stats widget
+begin_html
Shows character count, word count, and link count for the current note.
** Outline widget
+begin_html
Displays the heading structure of the note. Click headings to navigate.
** Backlinks widget
+begin_html
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
** Links widget
+begin_html
Shows notes that the current note links to.
- Widget Registration
Widgets are registered with =vulpea-ui-register-widget=, which allows filtering by predicate and ordering.
** Built-in widgets
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= |
** Registering widgets
+begin_src emacs-lisp
(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 100
+end_src
** Modifying widget properties
=vulpea-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.
+begin_src emacs-lisp
;; 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)
+end_src
** Toggling a widget per note
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.
+begin_src emacs-lisp
(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)
+end_src
The same pattern works for any widget (=stats=, =backlinks=, =links=, or your own).
** How it works
- Widgets are filtered by their =:predicate= (if any) against the current note
- Remaining widgets are sorted by =:order= (ascending)
- Each widget's =:component= is rendered
This allows packages like =vulpea-journal= to register context-specific widgets that only appear when viewing certain notes.
- Custom widgets
Create custom widgets using vui's =vui-defcomponent= macro:
+begin_src emacs-lisp
(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")))))
+end_src
Register the widget:
+begin_src emacs-lisp
(vulpea-ui-register-widget 'my-widget :component 'my-custom-widget :order 250) ; after outline, before backlinks
+end_src
For context-specific widgets (e.g., only for notes with a certain tag):
+begin_src emacs-lisp
(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)
+end_src
- Utility Functions
** vulpea-ui-clean-org-markup
Cleans org-mode markup from text for display purposes:
+begin_src emacs-lisp
(vulpea-ui-clean-org-markup text)
+end_src
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.
- Faces
| 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 |
- Related Projects
- [[https://github.com/d12frosted/vulpea-journal][vulpea-journal]] - Daily journaling with calendar, navigation, and "on this day" widgets
- [[https://github.com/d12frosted/ace-link-vui][ace-link-vui]] - Ace-link style navigation for VUI buffers
- License
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.
- Support
If you enjoy this project, you can support its development via [[https://github.com/sponsors/d12frosted][GitHub Sponsors]] or [[https://www.patreon.com/d12frosted][Patreon]].