From 95dc4b5db4f87f54e918f1c49d9ecd7432a74bd8 Mon Sep 17 00:00:00 2001 From: James Brechtel Date: Sun, 21 Jun 2026 11:43:56 -0400 Subject: [PATCH] feat: add spine-set-status, spine-add-note, spine-set-rating --- spine.el | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/spine.el b/spine.el index 1405908..8535d59 100644 --- a/spine.el +++ b/spine.el @@ -250,6 +250,81 @@ Signals an error on failure." t) (kill-buffer)))))) +(defun spine-set-status (id status) + "Set the TODO state of book with ID to STATUS. +STATUS should be one of: WANT, READING, READ, ABANDONED. +Signals an error if the book is not found." + (let ((org-file (expand-file-name spine-org-file))) + (unless (file-exists-p org-file) + (error "spine-set-status: file not found: %s" org-file)) + (with-current-buffer (find-file-noselect org-file) + (unwind-protect + (progn + (org-element-map (org-element-parse-buffer 'headline) 'headline + (lambda (hl) + (when (= (org-element-property :level hl) 1) + (let ((pos (org-element-property :begin hl))) + (when (equal id (spine--prop pos "ID")) + (goto-char pos) + (org-todo status))))) + nil 'headline) + (unless (eq (point) (point-min)) + (save-buffer) + t) + (when (eq (point) (point-min)) + (error "spine-set-status: no book found with ID %s" id))) + (kill-buffer))))) + +(defun spine-add-note (id text &optional date) + "Append a reading note to the book with ID. +TEXT is the note content. DATE is an optional YYYY-MM-DD string +\(defaults to today). Signals an error if the book is not found." + (let ((org-file (expand-file-name spine-org-file)) + (date-str (or date (format-time-string "%Y-%m-%d")))) + (unless (file-exists-p org-file) + (error "spine-add-note: file not found: %s" org-file)) + (with-current-buffer (find-file-noselect org-file) + (unwind-protect + (progn + (org-element-map (org-element-parse-buffer 'headline) 'headline + (lambda (hl) + (when (= (org-element-property :level hl) 1) + (let ((hl-id (spine--prop (org-element-property :begin hl) "ID"))) + (when (equal id hl-id) + (goto-char (org-element-property :end hl)) + (skip-chars-backward " \t\n") + (insert (format "\n- [%s] %s\n" date-str text)))))) + nil 'headline) + (unless (eq (point) (point-min)) + (save-buffer) + t) + (when (eq (point) (point-min)) + (error "spine-add-note: no book found with ID %s" id))) + (kill-buffer))))) + +(defun spine-set-rating (id rating) + "Set the RATING property of book with ID to RATING (1-5 string). +Signals an error if the book is not found." + (let ((org-file (expand-file-name spine-org-file))) + (unless (file-exists-p org-file) + (error "spine-set-rating: file not found: %s" org-file)) + (with-current-buffer (find-file-noselect org-file) + (unwind-protect + (progn + (org-element-map (org-element-parse-buffer 'headline) 'headline + (lambda (hl) + (when (= (org-element-property :level hl) 1) + (let ((pos (org-element-property :begin hl))) + (when (equal id (spine--prop pos "ID")) + (goto-char pos) + (org-set-property "RATING" rating))))) + nil 'headline) + (unless (eq (point) (point-min)) + (save-buffer) + t) + (when (eq (point) (point-min)) + (error "spine-set-rating: no book found with ID %s" id))) + (kill-buffer))))) (defun spine--extract-notes (hl-begin hl-end) "Extract reading notes from headline body between HL-BEGIN and HL-END. Returns list of (date-string text-string) pairs."