A journaling interface for vulpea that integrates seamlessly with vulpea-ui sidebar.
vulpea-journal brings the power of journaling to your vulpea-based note system. Think of it as 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
vulpea-journal requires:
(package-install 'vulpea-journal)(straight-use-package 'vulpea-journal)(elpaca vulpea-journal)(use-package vulpea-journal
:after (vulpea vulpea-ui)
:config
(vulpea-journal-setup))Journal widgets automatically register with vulpea-ui when you load vulpea-journal-ui. They 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 below.
(global-set-key (kbd "C-c j") #'vulpea-journal)Press C-c j to open today’s journal. The sidebar will show journal-specific widgets.
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
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.
Customize how journal notes are created. Use the template builder functions for convenience:
(setq vulpea-journal-default-template
(vulpea-journal-template-daily))This is equivalent to the default behavior. You can override any parameter:
(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"))(setq vulpea-journal-default-template
(vulpea-journal-template-monthly))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:
(setq vulpea-journal-default-template
(vulpea-journal-template-monthly
:file-name "journal/%Y/%m.org"
:entry-title "%d %A"))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) |
You can also provide a raw plist directly:
(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 bodyImportant: 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 %<format> syntax and are expanded at creation time.
For more control, use a function:
(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")))));; Start week on Sunday (0) or Monday (1, default)
(setq vulpea-journal-ui-calendar-week-start 1);; Include journal notes in the "created today" list
(setq vulpea-journal-ui-created-today-exclude-journal nil) ; default: t;; 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: tCustomize where journal widgets appear relative to vulpea-ui widgets:
(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)))Example: Move calendar before stats:
(use-package vulpea-journal
:custom
(vulpea-journal-ui-widget-orders
'((nav . 50)
(calendar . 90) ; now before stats
(created-today . 350)
(previous-years . 360))))Reference orders for vulpea-ui widgets: stats=100, outline=200, backlinks=300, links=400.
| 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 |
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 |
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.
Shows the current journal date and navigation buttons:
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.
Interactive month calendar:
- 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.
Lists all notes created on the journal’s date (from the CREATED property):
Click a note to visit it. Times come from the CREATED property timestamp.
Shows journal entries from the same date in previous years:
Click the date to visit that journal. Click ▸=/=▾ to expand/collapse the preview.
(add-hook 'emacs-startup-hook #'vulpea-journal)Use vulpea-journal-dates-in-range to query entries:
(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)))If you want a different tag than "journal":
;; Daily with custom tag
(setq vulpea-journal-default-template
(vulpea-journal-template-daily :tags '("daily-note")))
;; Monthly with custom tag
(setq vulpea-journal-default-template
(vulpea-journal-template-monthly :tags '("daily-note")))The first tag in the list is used for identification.
- Ensure
vulpea-journal-uiis 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
vulpea-journal extracts dates from the CREATED property. Ensure your template includes:
:head "#+created: %<[%Y-%m-%d]>"Supported formats:
[2025-12-08][2025-12-08 08:54]2025-12-08
Ensure vulpea-journal-setup is called in your config. This adds the necessary hooks:
(add-hook 'calendar-today-visible-hook #'vulpea-journal-calendar-mark-entries)
(add-hook 'calendar-today-invisible-hook #'vulpea-journal-calendar-mark-entries)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-modeto toggle it - Ensure
vulpea-journal-setupwas called before opening the calendar
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 runM-x vulpea-db-syncto 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.
If you have many existing journal files that need :ID: and CREATED properties, you can use this script to migrate them automatically.
WARNING: Back up your notes before running this script!
(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).
After running this, execute `vulpea-db-sync' 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)))
;; 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 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_MM_DD
((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))))))))))Usage:
- Back up your journal directory
- Evaluate the code above
- Run
M-x vulpea-journal-migrate-filesand select your journal directory - Run
M-x vulpea-db-syncto index the migrated files
;; 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 yearsvulpea-journal-calendar-entry-face- Calendar days with entriesvulpea-journal-ui-widget-title- Widget headersvulpea-journal-ui-calendar-date- Regular calendar daysvulpea-journal-ui-calendar-today- Today in calendarvulpea-journal-ui-calendar-entry- Days with entries in widgetvulpea-journal-ui-calendar-selected- Selected day in widget
Contributions welcome! Please open issues and PRs on GitHub.
GPLv3. See LICENSE for details.
If you enjoy this project, you can support its development via GitHub Sponsors or Patreon.



