Skip to content

Instantly share code, notes, and snippets.

@d12frosted
Last active October 24, 2024 20:18
Show Gist options
  • Save d12frosted/a60e8ccb9aceba031af243dff0d19b2e to your computer and use it in GitHub Desktop.
Save d12frosted/a60e8ccb9aceba031af243dff0d19b2e to your computer and use it in GitHub Desktop.
(defun vulpea-project-p ()
"Return non-nil if current buffer has any todo entry.
TODO entries marked as done are ignored, meaning the this
function returns nil if current buffer contains only completed
tasks."
(seq-find ; (3)
(lambda (type)
(eq type 'todo))
(org-element-map ; (2)
(org-element-parse-buffer 'headline) ; (1)
'headline
(lambda (h)
(org-element-property :todo-type h)))))
(defun vulpea-project-update-tag ()
"Update PROJECT tag in the current buffer."
(when (and (not (active-minibuffer-window))
(vulpea-buffer-p))
(save-excursion
(goto-char (point-min))
(let* ((tags (vulpea-buffer-tags-get))
(original-tags tags))
(if (vulpea-project-p)
(setq tags (cons "project" tags))
(setq tags (remove "project" tags)))
;; cleanup duplicates
(setq tags (seq-uniq tags))
;; update tags if changed
(when (or (seq-difference tags original-tags)
(seq-difference original-tags tags))
(apply #'vulpea-buffer-tags-set tags))))))
(defun vulpea-buffer-p ()
"Return non-nil if the currently visited buffer is a note."
(and buffer-file-name
(eq major-mode 'org-mode)
(string-suffix-p "org" buffer-file-name)
(string-prefix-p
(expand-file-name (file-name-as-directory vulpea-directory))
(file-name-directory buffer-file-name))))
(defun vulpea-project-files ()
"Return a list of note files containing 'project' tag." ;
(seq-uniq
(seq-map
#'car
(org-roam-db-query
[:select [nodes:file]
:from tags
:left-join nodes
:on (= tags:node-id nodes:id)
:where (like tag (quote "%\"project\"%"))]))))
(defun vulpea-agenda-files-update (&rest _)
"Update the value of `org-agenda-files'."
(setq org-agenda-files (vulpea-project-files)))
(add-hook 'find-file-hook #'vulpea-project-update-tag)
(add-hook 'before-save-hook #'vulpea-project-update-tag)
(advice-add 'org-agenda :before #'vulpea-agenda-files-update)
(advice-add 'org-todo-list :before #'vulpea-agenda-files-update)
;; functions borrowed from `vulpea' library
;; https://github.com/d12frosted/vulpea/blob/6a735c34f1f64e1f70da77989e9ce8da7864e5ff/vulpea-buffer.el
(defun vulpea-buffer-tags-get ()
"Return filetags value in current buffer."
(vulpea-buffer-prop-get-list "filetags" "[ :]"))
(defun vulpea-buffer-tags-set (&rest tags)
"Set TAGS in current buffer.
If filetags value is already set, replace it."
(if tags
(vulpea-buffer-prop-set
"filetags" (concat ":" (string-join tags ":") ":"))
(vulpea-buffer-prop-remove "filetags")))
(defun vulpea-buffer-tags-add (tag)
"Add a TAG to filetags in current buffer."
(let* ((tags (vulpea-buffer-tags-get))
(tags (append tags (list tag))))
(apply #'vulpea-buffer-tags-set tags)))
(defun vulpea-buffer-tags-remove (tag)
"Remove a TAG from filetags in current buffer."
(let* ((tags (vulpea-buffer-tags-get))
(tags (delete tag tags)))
(apply #'vulpea-buffer-tags-set tags)))
(defun vulpea-buffer-prop-set (name value)
"Set a file property called NAME to VALUE in buffer file.
If the property is already set, replace its value."
(setq name (downcase name))
(org-with-point-at 1
(let ((case-fold-search t))
(if (re-search-forward (concat "^#\\+" name ":\\(.*\\)")
(point-max) t)
(replace-match (concat "#+" name ": " value) 'fixedcase)
(while (and (not (eobp))
(looking-at "^[#:]"))
(if (save-excursion (end-of-line) (eobp))
(progn
(end-of-line)
(insert "\n"))
(forward-line)
(beginning-of-line)))
(insert "#+" name ": " value "\n")))))
(defun vulpea-buffer-prop-set-list (name values &optional separators)
"Set a file property called NAME to VALUES in current buffer.
VALUES are quoted and combined into single string using
`combine-and-quote-strings'.
If SEPARATORS is non-nil, it should be a regular expression
matching text that separates, but is not part of, the substrings.
If nil it defaults to `split-string-default-separators', normally
\"[ \f\t\n\r\v]+\", and OMIT-NULLS is forced to t.
If the property is already set, replace its value."
(vulpea-buffer-prop-set
name (combine-and-quote-strings values separators)))
(defun vulpea-buffer-prop-get (name)
"Get a buffer property called NAME as a string."
(org-with-point-at 1
(when (re-search-forward (concat "^#\\+" name ": \\(.*\\)")
(point-max) t)
(buffer-substring-no-properties
(match-beginning 1)
(match-end 1)))))
(defun vulpea-buffer-prop-get-list (name &optional separators)
"Get a buffer property NAME as a list using SEPARATORS.
If SEPARATORS is non-nil, it should be a regular expression
matching text that separates, but is not part of, the substrings.
If nil it defaults to `split-string-default-separators', normally
\"[ \f\t\n\r\v]+\", and OMIT-NULLS is forced to t."
(let ((value (vulpea-buffer-prop-get name)))
(when (and value (not (string-empty-p value)))
(split-string-and-unquote value separators))))
(defun vulpea-buffer-prop-remove (name)
"Remove a buffer property called NAME."
(org-with-point-at 1
(when (re-search-forward (concat "\\(^#\\+" name ":.*\n?\\)")
(point-max) t)
(replace-match ""))))
@d12frosted
Copy link
Author

@bmp it's not related to the shared code, this is related to the way org-agenda works. Basically, org-agenda needs to open and parse each file from org-agenda-files in order to build the agenda buffer. I haven't developed org-agenda, so can't say for sure but I see no reasons to close these files considering that (a) agenda buffer is interactive and agenda actions write to buffer, and opening files is not free and (b) it's unclear which files it needs to close - what if user opened a file that is part of agenda, should agenda close it or not? IMO consistency in the later case is crucial.

@bmp
Copy link

bmp commented Sep 10, 2024

@d12frosted Thank you for the detailed explanation. Makes sense! Also, I was under the impression that tags in org-roam can be separated by space now and don't need :.

@d12frosted
Copy link
Author

@bmp but then you need to move tags from #+filetags to #+roam_tags for space-separation to work. As you can see, in this article we do set tags in filetags, which are :-delimited. So adapt the flow according to your use-case 😸 👍

@bmp
Copy link

bmp commented Sep 10, 2024

Thank you! :-)

@bmp
Copy link

bmp commented Sep 14, 2024

One more question, in case I am using org-roam-dailies, which stores the tasks in individual files within a sub-folder daily within the org-roam folder, do I need to update the function vulpea-agenda-files-update to include the daily folder? Or would it be simpler to modify the capture function for org-roam-dailies-capture-templates to add the project tag?

@akashpal-21
Copy link

akashpal-21 commented Sep 14, 2024

One more question, in case I am using org-roam-dailies, which stores the tasks in individual files within a sub-folder daily within the org-roam folder, do I need to update the function vulpea-agenda-files-update to include the daily folder? Or would it be simpler to modify the capture function for org-roam-dailies-capture-templates to add the project tag?

Vulpea scans the buffer for any todo and adds the project tag automatically. It latches onto before-save hook.

(add-hook 'before-save-hook #'vulpea-project-update-tag)

(defun vulpea-project-update-tag ()
    "Update PROJECT tag in the current buffer."
  ...
        (goto-char (point-min))
        (let* ((tags (vulpea-buffer-tags-get))
               (original-tags tags))
          (if (vulpea-project-p)
              (setq tags (cons "project" tags))
            (setq tags (remove "project" tags)))
...

Hey @d12frosted Would you consider updating the gist with this
https://gist.github.com/d12frosted/a60e8ccb9aceba031af243dff0d19b2e?permalink_comment_id=4976115#gistcomment-4976115
? Or give your opinion - last time I remember it was borking pdf files from opening in emacs 29.

Thanks.

@bmp
Copy link

bmp commented Sep 15, 2024

@akashpal-21 Thanks, that makes sense. I had missed it as when I was looking at my agenda, the tasks wouldn't show up, but then I realised it was because I had customized the agenda view to only show tasks with deadlines in the current week. Changing the view to all tasks with todo did indeed show up the other tasks.

@d12frosted
Copy link
Author

@akashpal-21 updated the gist. Thanks for reminder :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment