A daily journaling interface for vulpea that integrates seamlessly with vulpea-ui sidebar.
vulpea-journal brings the power of daily 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:
- Daily notes - One file per day, automatically created with customizable templates
- 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:
;; With use-package and straight.el
(use-package vulpea-journal
:straight (:host github :repo "d12frosted/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"). Each note has:
- A file path based on the date (e.g.,
journal/2025-12-08.org) - A title based on the date (e.g.,
2025-12-08 Sunday) - 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
- 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:
(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 and :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 day (creating the entry if needed). Clicking Today jumps to today’s journal.
Interactive month calendar:
- Bold = today
- Highlighted = selected date
- Dot (·) = has journal entry
- Click any date to open its journal
Use < and > 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":
(setq vulpea-journal-default-template
'(:file-name "daily/%Y-%m-%d.org"
:title "%Y-%m-%d %A"
:tags ("daily-note") ; First tag is used for identification
:head "#+created: %<[%Y-%m-%d]>"))- 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
;; 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.



