+title: vulpea-journal
+author: Boris Buliga
+begin_html
#+end_htmlA journaling interface for [[https://github.com/d12frosted/vulpea][vulpea]] that integrates seamlessly with [[https://github.com/d12frosted/vulpea-ui][vulpea-ui]] sidebar.
- What is vulpea-journal?
vulpea-journal brings the power of journaling to your vulpea-based note system. Think of it as [[https://github.com/bastibe/org-journal][org-journal]] rebuilt from the ground up for vulpea, with a modern reactive UI.
Key features:
- Flexible granularity - One file per day, or one file per month with daily headings
- Sidebar widgets - Calendar, navigation, related notes - all in the vulpea-ui sidebar
- Calendar integration - See which days have entries, jump to any date
- Previous years - "On this day" view showing what you wrote in past years
- Zero window management - Uses vulpea-ui sidebar, no custom layouts to break
- Quick Start
** 1. Install
vulpea-journal requires:
- Emacs 29.1+
- [[https://github.com/d12frosted/vulpea][vulpea]] 2.0+
- [[https://github.com/d12frosted/vulpea-ui][vulpea-ui]] 1.0+
- [[https://github.com/magnars/dash.el][dash]] 2.20+
*** Using package.el (MELPA)
+begin_src emacs-lisp
(package-install 'vulpea-journal)
+end_src
*** Using straight.el
+begin_src emacs-lisp
(straight-use-package 'vulpea-journal)
+end_src
*** Using elpaca
+begin_src emacs-lisp
(elpaca vulpea-journal)
+end_src
*** Using Doom Emacs
In =packages.el=:
+begin_src emacs-lisp
(package! vulpea-journal)
+end_src
In =config.el=:
+begin_src emacs-lisp
(use-package! vulpea-journal :after (vulpea vulpea-ui) :config (vulpea-journal-setup))
+end_src
*** Configuration
+begin_src emacs-lisp
(use-package vulpea-journal :after (vulpea vulpea-ui) :config (vulpea-journal-setup))
+end_src
** 2. Widgets are auto-registered
=vulpea-journal-setup= automatically loads =vulpea-journal-ui= and registers all sidebar widgets — you don't need to require it separately. Widgets only appear when viewing journal notes.
Default widget order interleaves with vulpea-ui widgets:
| Widget | Order | Appears... | |------------------+-------+---------------------| | journal-nav | 50 | Before stats | | stats | 100 | (vulpea-ui) | | journal-calendar | 150 | After stats | | outline | 200 | (vulpea-ui) | | backlinks | 300 | (vulpea-ui) | | created-today | 350 | After backlinks | | previous-years | 360 | After created-today | | links | 400 | (vulpea-ui) |
To customize the order, see [[*Widget Order Configuration][Widget Order Configuration]] below.
** 3. Add a keybinding
+begin_src emacs-lisp
(global-set-key (kbd "C-c j") #'vulpea-journal)
+end_src
** 4. Start journaling!
Press =C-c j= to open today's journal. The sidebar will show journal-specific widgets.
- How It Works
** Journal Notes
Journal notes are regular vulpea notes identified by a tag (default: ="journal"=). Depending on the template granularity:
- Daily (default): one file per day (e.g., =journal/2025-12-08.org=)
- Monthly: one file per month (e.g., =journal/2025-12.org=) with daily entries as headings (e.g., =* 08 Monday=)
Each note has a CREATED property storing the date for querying.
When you call =vulpea-journal=, it:
- Finds or creates the note for that date
- Opens it in your main window (for monthly, navigates to the heading)
- Shows the vulpea-ui sidebar with journal widgets
** Sidebar Integration
vulpea-journal doesn't create its own windows. Instead, it provides widgets that plug into vulpea-ui's sidebar system. This means:
- No window management bugs
- Consistent UI with the rest of vulpea-ui
- Widgets automatically appear/disappear based on what you're viewing
Journal widgets check if the current note is a journal entry. When you view a non-journal note, they simply hide themselves.
- Configuration
** Template
Customize how journal notes are created. Use the template builder functions for convenience:
*** Daily (one file per day, default)
+begin_src emacs-lisp
(setq vulpea-journal-default-template (vulpea-journal-template-daily))
+end_src
This is equivalent to the default behavior. You can override any parameter:
+begin_src emacs-lisp
(setq vulpea-journal-default-template (vulpea-journal-template-daily :file-name "daily/%Y-%m-%d.org" :title "%A, %B %d, %Y" :body " Morning\n\n Evening\n"))
+end_src
*** Monthly (one file per month)
+begin_src emacs-lisp
(setq vulpea-journal-default-template (vulpea-journal-template-monthly))
+end_src
This creates one file per month (e.g., =journal/2025-12.org=) with each day as a heading (e.g., =* 08 Monday=). Override any parameter:
+begin_src emacs-lisp
(setq vulpea-journal-default-template (vulpea-journal-template-monthly :file-name "journal/%Y/%m.org" :entry-title "%d %A"))
+end_src
Monthly template parameters:
| Parameter | Default | Description | |-----------------+--------------------+----------------------------------| | =:file-name= | =journal/%Y-%m.org= | strftime format for monthly file | | =:title= | =%Y-%m= | File-level note title | | =:entry-level= | =1= | Heading level for daily entries | | =:entry-title= | =%d %A= | strftime format for heading | | =:tags= | =("journal")= | Tags (first one identifies journals) |
*** Raw template plist
You can also provide a raw plist directly:
+begin_src emacs-lisp
(setq vulpea-journal-default-template '(:file-name "journal/%Y-%m-%d.org" ; File path (strftime format) :title "%Y-%m-%d %A" ; Note title (strftime format) :tags ("journal") ; Tags (first one identifies journals) :head "#+created: %<[%Y-%m-%d]>" ; Header content :body " Morning\n\n Evening\n")) ; Initial body
+end_src
Important: The =:file-name=, =:title=, and =:entry-title= use =strftime= format because they're expanded for the /target date/, not the current time. When you open the journal for December 25th, the file will be =journal/2025-12-25.org= regardless of today's date.
Other keys (=:head=, =:body=) use vulpea's =%
*** Dynamic Templates
For more control, use a function:
+begin_src emacs-lisp
(setq vulpea-journal-default-template (lambda (date) (let ((weekday (format-time-string "%u" date))) (list :file-name "journal/%Y-%m-%d.org" :title (format-time-string "%Y-%m-%d %A" date) :tags '("journal") :head "#+created: %<[%Y-%m-%d]>" :body (if (member weekday '("6" "7")) " Weekend\n\n" " Work\n\n* Personal\n")))))
+end_src
** Calendar Widget
+begin_src emacs-lisp
;; Start week on Sunday (0) or Monday (1, default) (setq vulpea-journal-ui-calendar-week-start 1)
+end_src
** Created Today Widget
+begin_src emacs-lisp
;; Include journal notes in the "created today" list (setq vulpea-journal-ui-created-today-exclude-journal nil) ; default: t
+end_src
** Previous Years Widget
+begin_src emacs-lisp
;; How many years to look back (setq vulpea-journal-ui-previous-years-count 5) ; default: 5
;; Characters to show in preview (setq vulpea-journal-ui-previous-years-preview-chars 256)
;; Hide org drawers in preview (setq vulpea-journal-ui-previous-years-hide-drawers t) ; default: t
;; Start with previews expanded (setq vulpea-journal-ui-previous-years-expanded t) ; default: t
+end_src
** Widget Order Configuration
Customize where journal widgets appear relative to vulpea-ui widgets:
+begin_src emacs-lisp
(setq vulpea-journal-ui-widget-orders '((nav . 50) ; before stats (100) (calendar . 150) ; after stats, before outline (200) (created-today . 350) ; after backlinks (300) (previous-years . 360)))
+end_src
Example: Move calendar before stats:
+begin_src emacs-lisp
(use-package vulpea-journal :custom (vulpea-journal-ui-widget-orders '((nav . 50) (calendar . 90) ; now before stats (created-today . 350) (previous-years . 360))))
+end_src
Reference orders for vulpea-ui widgets: stats=100, outline=200, backlinks=300, links=400.
- Commands
| Command | Description | |---------------------------+---------------------------------------------------------| | =vulpea-journal= | Open today's journal (or specify date programmatically) | | =vulpea-journal-today= | Open today's journal | | =vulpea-journal-date= | Prompt for a date and open its journal | | =vulpea-journal-next= | Go to next journal entry | | =vulpea-journal-previous= | Go to previous journal entry | | =vulpea-journal-setup= | Enable calendar/sidebar integration and register widgets |
- Sidebar Keybindings
When viewing a journal note, the sidebar provides quick navigation:
| Key | Action | |-----+--------------------------------| | =[= | Go to previous journal entry | | =]= | Go to next journal entry | | =t= | Go to today's journal | | =d= | Pick a date |
- Calendar Integration
After calling =vulpea-journal-setup=, the Emacs calendar gains journal superpowers via =vulpea-journal-calendar-mode=:
| Key | Action | |-----+--------------------------------| | =j= | Open journal for date at point | | =]= | Jump to next journal entry | | =[= | Jump to previous journal entry |
Days with journal entries are highlighted in the calendar.
The minor mode is automatically enabled in calendar buffers. You can toggle it with =M-x vulpea-journal-calendar-mode= if needed.
- Widgets Reference
** vulpea-journal-widget-nav
Shows the current journal date and navigation buttons:
[[file:images/vulpea-journal-ui-navigation.png]]
Clicking Prev/Next navigates by one calendar day (creating the entry if needed). Clicking Today jumps to today's journal.
Note: The sidebar keybindings =[= and =]= behave differently — they jump to the next/previous existing journal entry, skipping days without entries.
** vulpea-journal-widget-calendar
Interactive month calendar:
[[file:images/vulpea-journal-ui-calendar.png]]
- Bold = today
- Highlighted = selected date
- Dot (·) = has journal entry
- Click any date to open its journal
Click the =<= and =>= buttons to navigate months without changing the selected date.
** vulpea-journal-widget-created-today
Lists all notes created on the journal's date (from the CREATED property):
[[file:images/vulpea-journal-ui-created-today.png]]
Click a note to visit it. Times come from the CREATED property timestamp.
** vulpea-journal-widget-previous-years
Shows journal entries from the same date in previous years:
[[file:images/vulpea-journal-ui-previous-years.png]]
Click the date to visit that journal. Click =▸=/=▾= to expand/collapse the preview.
- Tips & Tricks
** Open journal on Emacs startup
+begin_src emacs-lisp
(add-hook 'emacs-startup-hook #'vulpea-journal)
+end_src
** Weekly review workflow
Use =vulpea-journal-dates-in-range= to query entries:
+begin_src emacs-lisp
(defun my/journal-this-week () "Get all journal dates from this week." (let* ((today (current-time)) (dow (string-to-number (format-time-string "%u" today))) (start (time-subtract today (days-to-time (1- dow)))) (end (time-add start (days-to-time 7)))) (vulpea-journal-dates-in-range start end)))
+end_src
** Custom journal tag
If you want a different tag than ="journal"=, set =vulpea-journal-tag=:
+begin_src emacs-lisp
(setq vulpea-journal-tag "daily-note")
+end_src
Template builders (=vulpea-journal-template-daily=, =vulpea-journal-template-monthly=) use =vulpea-journal-tag= as the default tag automatically. If you use a raw plist for =vulpea-journal-default-template=, make sure the first element of =:tags= matches =vulpea-journal-tag=.
- Troubleshooting
** Widgets don't appear in sidebar
- Ensure =vulpea-journal-ui= is loaded (widgets auto-register on load)
- Check that you're viewing a journal note (has the journal tag)
- Try =M-x vulpea-ui-sidebar-refresh=
** Date extraction fails
vulpea-journal extracts dates from the CREATED property. Ensure your template includes:
+begin_src emacs-lisp
:head "#+created: %<[%Y-%m-%d]>"
+end_src
Supported formats:
- =[2025-12-08]=
- =[2025-12-08 08:54]=
- =2025-12-08=
** Calendar marks don't appear
Ensure =vulpea-journal-setup= is called in your config. This adds the necessary hooks:
+begin_src emacs-lisp
(add-hook 'calendar-today-visible-hook #'vulpea-journal-calendar-mark-entries) (add-hook 'calendar-today-invisible-hook #'vulpea-journal-calendar-mark-entries)
+end_src
** Calendar keybindings don't work
=vulpea-journal-setup= enables =vulpea-journal-calendar-mode= in calendar buffers. If keybindings aren't working:
- Verify the minor mode is active by running =M-x vulpea-journal-calendar-mode= to toggle it
- Ensure =vulpea-journal-setup= was called before opening the calendar
** Existing journal files not in database
If you have existing journal files that are not properly indexed in the vulpea database (e.g., manually created files, or files without an =:ID:= property), vulpea-journal will show an error when you try to navigate to that date. This is a safety measure to prevent accidentally overwriting your files.
To fix this, you have two options:
-
Add proper properties and sync: Ensure each file has an =:ID:= property in its property drawer, then run =M-x vulpea-db-sync-full-scan= to index them.
-
Delete and recreate: If the files don't contain important content, delete them and let vulpea-journal create new ones.
Additionally, if your notes lack the =CREATED= property, vulpea-journal won't find them for date-based queries (like "notes created today"). The note itself will work fine if opened directly.
** Migration script
If you have many existing journal files that need =:ID:=, =CREATED= properties, and the journal filetag, you can use this script to migrate them automatically.
WARNING: Back up your notes before running this script!
+begin_src emacs-lisp
(defun vulpea-journal-migrate-files (directory) "Migrate journal files in DIRECTORY to vulpea-journal format. Adds :ID: property if missing. Adds CREATED property if missing (inferred from filename or title). Adds journal filetag if missing (using `vulpea-journal-tag').
After running this, execute `vulpea-db-sync-full-scan' to index the files." (interactive "DJournal directory: ") (let ((files (directory-files directory t "\.org$")) (count (length files)) (modified 0) (skipped 0)) (message "Processing %d files in %s..." count directory) (dolist (file files) (message " Processing %s..." (file-name-nondirectory file)) (with-current-buffer (find-file-noselect file) (let ((changed nil) (filename (file-name-base file))) ;; Check/add ID property (goto-char (point-min)) (unless (org-entry-get (point) "ID") (org-id-get-create) (setq changed t) (message " Added ID")) ;; Check/add CREATED property (goto-char (point-min)) (unless (org-entry-get (point) "CREATED") (when-let ((date (vulpea-journal-migrate--infer-date file))) (org-set-property "CREATED" date) (setq changed t) (message " Added CREATED: %s" date))) ;; Check/add journal filetag (goto-char (point-min)) (unless (member vulpea-journal-tag (vulpea-buffer-tags-get)) (vulpea-buffer-tags-add (list vulpea-journal-tag)) (setq changed t) (message " Added filetag: %s" vulpea-journal-tag)) ;; Save if modified (if changed (progn (save-buffer) (setq modified (1+ modified))) (setq skipped (1+ skipped))) (kill-buffer)))) (message "Migration complete: %d modified, %d skipped" modified skipped) (message "Run M-x vulpea-db-sync-full-scan to index the migrated files.")))
(defun vulpea-journal-migrate--infer-date (file) "Infer date from FILE path or title. Returns date string in format [YYYY-MM-DD] or nil." (let ((filename (file-name-base file))) (cond ;; Try common filename patterns: YYYYMMDD, YYYY-MM-DD, YYYY_MMDD ((string-match "\([0-9]\{4\}\)[-]?\([0-9]\{2\}\)[-]?\([0-9]\{2\}\)" filename) (format "[%s-%s-%s]" (match-string 1 filename) (match-string 2 filename) (match-string 3 filename))) ;; Try reading title from file (t (with-temp-buffer (insert-file-contents file) (goto-char (point-min)) (when (re-search-forward "^#\+title:[ \t]*\(.+\)" nil t) (let ((title (match-string 1))) (when (string-match "\([0-9]\{4\}\)[-]?\([0-9]\{2\}\)[_-]?\([0-9]\{2\}\)" title) (format "[%s-%s-%s]" (match-string 1 title) (match-string 2 title) (match-string 3 title))))))))))
+end_src
Usage:
- Back up your journal directory
- Evaluate the code above
- Run =M-x vulpea-journal-migrate-files= and select your journal directory
- Run =M-x vulpea-db-sync-full-scan= to index the migrated files
- API Reference
** Functions
+begin_src emacs-lisp
;; Template builders (vulpea-journal-template-daily) ; Daily template (one file per day) (vulpea-journal-template-monthly) ; Monthly template (one file per month)
;; Note identification (vulpea-journal-note-p note) ; Is NOTE a journal note? (vulpea-journal-note-date note) ; Extract date from journal NOTE
;; Note retrieval (vulpea-journal-note date) ; Get/create journal for DATE (vulpea-journal-find-note date) ; Find existing journal (no create)
;; Queries (vulpea-journal-all-dates) ; All dates with journals (vulpea-journal-dates-in-month m y) ; Journals in month M of year Y (vulpea-journal-dates-in-range a b) ; Journals between dates A and B (vulpea-journal-notes-for-date-across-years date n) ; Same date in past N years
+end_src
** Faces
- =vulpea-journal-calendar-entry-face= - Calendar days with entries
- =vulpea-journal-ui-widget-title= - Widget headers
- =vulpea-journal-ui-calendar-date= - Regular calendar days
- =vulpea-journal-ui-calendar-today= - Today in calendar
- =vulpea-journal-ui-calendar-entry= - Days with entries in widget
- =vulpea-journal-ui-calendar-selected= - Selected day in widget
- Contributing
Contributions welcome! Please open issues and PRs on GitHub.
- License
GPLv3. See [[file:LICENSE][LICENSE]] for details.
- 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]].