summaryrefslogtreecommitdiff
path: root/lisp/ss-capture.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/ss-capture.el')
-rw-r--r--lisp/ss-capture.el175
1 files changed, 175 insertions, 0 deletions
diff --git a/lisp/ss-capture.el b/lisp/ss-capture.el
new file mode 100644
index 0000000..f930716
--- /dev/null
+++ b/lisp/ss-capture.el
@@ -0,0 +1,175 @@
+;;; ss-capture.el --- Capture configuration -*- lexical-binding: t; -*-
+
+;;; Commentary:
+
+;; Journal capture helpers and capture templates.
+
+;;; Code:
+
+(require 'calendar)
+(require 'org)
+(require 'org-capture)
+(require 'ss-core)
+(require 'ss-org)
+
+(defun ss-journal-capture-time ()
+ "Return the effective timestamp for the current journal capture."
+ (or org-overriding-default-time
+ (org-capture-get :default-time)
+ (current-time)))
+
+(defun ss-journal-calendar-date (&optional time)
+ "Return TIME as a Gregorian date list for datetree helpers."
+ (calendar-gregorian-from-absolute
+ (time-to-days (or time (current-time)))))
+
+(defun ss-journal-year-heading (&optional time)
+ "Return the journal year heading text for TIME."
+ (format-time-string "%Y" (or time (current-time))))
+
+(defun ss-journal-day-heading (&optional time)
+ "Return the journal day heading text for TIME."
+ (format-time-string "%Y-%m-%d %A" (or time (current-time))))
+
+(defun ss-journal-find-or-create-heading (level heading)
+ "Move to HEADING at LEVEL, creating it when missing."
+ (goto-char (point-min))
+ (if (re-search-forward
+ (format "^%s %s$"
+ (make-string level ?*)
+ (regexp-quote heading))
+ nil t)
+ (goto-char (match-beginning 0))
+ (goto-char (point-max))
+ (unless (bolp)
+ (insert "\n"))
+ (insert (make-string level ?*) " " heading "\n")
+ (forward-line -1))
+ (org-back-to-heading t))
+
+(defun ss-journal-goto-date (&optional time create)
+ "Move to TIME's journal date heading.
+When CREATE is non-nil, create the datetree entry when missing."
+ (goto-char (point-min))
+ (if create
+ (let ((year-heading (ss-journal-year-heading time))
+ (day-heading (ss-journal-day-heading time)))
+ (ss-journal-find-or-create-heading 1 year-heading)
+ (save-restriction
+ (org-narrow-to-subtree)
+ (ss-journal-find-or-create-heading 2 day-heading))
+ t)
+ (when (re-search-forward
+ (format "^\\*\\* %s$"
+ (regexp-quote
+ (ss-journal-day-heading (or time (current-time)))))
+ nil t)
+ (goto-char (match-beginning 0))
+ t)))
+
+(defun ss-journal-ensure-day-sections ()
+ "Ensure the standard section headings exist under the current journal day."
+ (org-back-to-heading t)
+ (let ((section-level (1+ (org-outline-level))))
+ (save-excursion
+ (save-restriction
+ (org-narrow-to-subtree)
+ (dolist (section ss-journal-section-headings)
+ (goto-char (point-min))
+ (unless (org-find-exact-headline-in-buffer section)
+ (goto-char (point-max))
+ (unless (bolp)
+ (insert "\n"))
+ (insert (make-string section-level ?*) " " section "\n")))))))
+
+(defun ss-journal-goto-section (section &optional time)
+ "Move to SECTION beneath TIME's journal date, creating structure as needed."
+ (unless (member section ss-journal-section-headings)
+ (user-error "Unknown journal section: %s" section))
+ (ss-journal-goto-date time 'create)
+ (ss-journal-ensure-day-sections)
+ (let ((section-level (1+ (org-outline-level)))
+ position)
+ (save-restriction
+ (org-narrow-to-subtree)
+ (goto-char (point-min))
+ (when (re-search-forward
+ (format "^%s %s$"
+ (make-string section-level ?*)
+ (regexp-quote section))
+ nil t)
+ (setq position (match-beginning 0))))
+ (unless position
+ (user-error "Journal section not found: %s" section))
+ (goto-char position)
+ (org-back-to-heading t)))
+
+(defun ss-journal-capture-target (section)
+ "Select SECTION under today's journal datetree entry for capture."
+ (set-buffer (find-file-noselect (ss-require-existing-file ss-journal-file)))
+ (widen)
+ (ss-journal-goto-section section (ss-journal-capture-time)))
+
+(defun ss-capture--denote-templates ()
+ "Return Denote-backed capture templates when Denote is enabled."
+ (when (ss-feature-enabled-p 'denote)
+ `(("n" "Denote")
+ ("nn" "Generic" plain
+ (file denote-last-path)
+ (function
+ (lambda ()
+ (denote-org-capture-with-prompts :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t)
+ ("np" "Project" plain
+ (file denote-last-path)
+ (function
+ (lambda ()
+ (ss-denote-capture-in-directory
+ ss-org-projects-directory '("project") :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t)
+ ("na" "Area" plain
+ (file denote-last-path)
+ (function
+ (lambda ()
+ (ss-denote-capture-in-directory
+ ss-org-areas-directory nil :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t)
+ ("nr" "Resource" plain
+ (file denote-last-path)
+ (function
+ (lambda ()
+ (ss-denote-capture-in-directory
+ ss-org-resources-directory nil :title :keywords :subdirectory)))
+ :no-save t
+ :immediate-finish nil
+ :kill-buffer t
+ :jump-to-captured t))))
+
+(defun ss-capture-setup ()
+ "Initialize capture templates."
+ (setq org-capture-templates
+ (append
+ '(("j" "Journal")
+ ("jt" "Task" entry
+ (function (lambda () (ss-journal-capture-target "Tasks")))
+ "* TODO %?")
+ ("jn" "Note" entry
+ (function (lambda () (ss-journal-capture-target "Notes")))
+ "* %?")
+ ("jm" "Meeting" entry
+ (function (lambda () (ss-journal-capture-target "Meetings")))
+ "* <%<%Y-%m-%d %H:%M>> %?"))
+ (ss-capture--denote-templates))))
+
+(provide 'ss-capture)
+
+;;; ss-capture.el ends here