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

@dm19 🤔 I thought of implementing a query that supports org-todo-keywords, but then realised that it will not support file-level overrides.

TODO keywords and interpretation can also be set on a per-file basis with
the special #+SEQ_TODO and #+TYP_TODO lines.

So instead I would rather add a separate column todo-type with values nil | todo | done to the table. Stay tuned :)

@Herschenglime
Copy link

Herschenglime commented Jan 29, 2022

Once I figured out how to integrate this into doom emacs, it worked really well, thanks! Below is how I have it setup in my config.el (with a (package! vulpea) in my packages.el):

(use-package! vulpea
  :after org-roam
  :config
  (load! "roam-agenda") ;; a separate file containing the gist in my private doom directory

  ;; prevent headings from clogging tag
  (add-to-list 'org-tags-exclude-from-inheritance "project")
  )

If I may make one suggestion though, I think it'd be a good idea to add

(advice-add 'org-todo-list :before #'vulpea-agenda-files-update)

after the advice-add on line 62, as the todo's are not properly indexed otherwise when using the SPC n t binding in doom that is bound to org-todo-list.

Thanks again!

@d12frosted
Copy link
Author

@Herschenglime makes sense. Seems like org-todo-list has its own routine. Will update Gist and post. Thanks for suggestion!

@DominikMendel
Copy link

How can I update vulpea-project-p such that (lambda (type) (eq type 'todo)) instead checks multiple todo states? I saw previously mentioning org-todo-keywords but I don't think it is implemented yet. How can I for now update that line of code to be something like "Equals 'todo OR 'next ' OR 'waiting"?

Also @d12frosted , everything you responded with to me previously works well. Thank you so much for all the help.

@Herschenglime
Copy link

Herschenglime commented Feb 2, 2022

Thanks for such a quick response! I have one more suggestion (keep in mind that I'm not an expert by any means):

After returning to my todo list today, I was surprised to see that the entries that I had changed the todo state of from the org agenda menu were back to the TODO status. I quickly realized that the changes to TODO state hadn't been saved, and after a quick google search I found this solution on reddit, which boils down to adding

  (add-hook 'org-trigger-hook 'save-buffer)

somewhere in your personal config. In this way, every time the state of a todo item is changed, the document that it originated from is immediately saved to reflect this.

As I use syncthing to keep my org documents up to date across my phone and laptop, this is particularly useful to me, although I haven't considered any scenarios in which autosaving might be a bad thing. Still, mentioning this somewhere in the gist or on the blog post could be helpful to others, especially considering that most people would probably expect this functionality anyways when toggling the todo state from the org agenda.

Thanks for your consideration!


EDIT: Nevermind, I discovered that this will save immediately not only while in the org agenda view, but also when toggling the state within the org document itself; not great.

A better solution that I came across is this:

  (advice-add 'org-agenda-todo :after #'org-save-all-org-buffers)

Which will invoke the function to save open org buffers only when the state is toggled from the org agenda view. I think the best implementation of this would involve only saving the file whose todo state was changed as opposed to every single open org file, but I'm not sure how I'd go about doing that; perhaps looking at how org-agenda-todo does it would help.

@DominikMendel
Copy link

Ignore my last comment. I didn't realize that (advice-add 'org-todo-list :before #'vulpea-agenda-files-update) achieved exactly what I was looking for. Works great!

@d12frosted
Copy link
Author

How can I update vulpea-project-p such that (lambda (type) (eq type 'todo)) instead checks multiple todo states? I saw previously mentioning org-todo-keywords but I don't think it is implemented yet. How can I for now update that line of code to be something like "Equals 'todo OR 'next ' OR 'waiting"?

I know you asked to ignore this comment, but I still want to emphasise that using elements API here is actually taking into account values from org-todo-keywords. So (eq type 'todo) is true for "TODO" state and for any other state that is not considered done. For example, my value of org-todo-keywords is:

((sequence "TODO(t)" "|" "DONE(d!)")
 (sequence "WAITING(w@/!)" "HOLD(h@/!)" "|" "CANCELLED(c@/!)" "MEETING"))

Meaning that "TODO", "WAITING" and "HOLD" are considered 'todo by this method, while "DONE", "CANCELLED" and "MEETING" are considered 'done.

And my comment about not taking into consideration was related to proposal to use org-roam-db directly.

@d12frosted
Copy link
Author

@Herschenglime Glad you figured that out! Indeed, you need to save your file for changes to apply :) btw, I don't use this automatic saving and rather press s in agenda or C-x s to save all modified buffers in Emacs. Maybe an unnecessary key press, but I find it more responsive to my taste.

@LuciusChen
Copy link

How do I do this with other agenda files?

@d12frosted
Copy link
Author

@LuciusChen

How do I do this with other agenda files?

What do you mean by other agenda files?

@LuciusChen
Copy link

some files not in org-roam-db and have no ID but in org-agenda

@d12frosted
Copy link
Author

@LuciusChen In case I understood you correctly, you want to have agenda that consists of org-roam files and non-org-roam files at the same. In that case you just need to modify the following function:

(defun vulpea-agenda-files-update (&rest _)
"Update the value of `org-agenda-files'."
  (setq org-agenda-files (vulpea-project-files)))

vulpea-project-files returns you a list of files, so you can make an union of two lists using append function. And just in case you have duplicates, you can use seq-unique:

(defun vulpea-agenda-files-update (&rest _)
  "Update the value of `org-agenda-files'."
  (setq org-agenda-files (seq-uniq
                          (append
                           (vulpea-project-files)
                           '("/path/to/file1"
                             "/path/to/file2"
                             "...")))))

Just put what you need 😄 If needed, you can move it to a configuration variable.

Does that answer your question?

@LuciusChen
Copy link

Wow, just what I was looking for, for which I searched in many ways without success, thank you.

@d12frosted
Copy link
Author

@LuciusChen glad to hear. Enjoy 😄

@Qkessler
Copy link

Qkessler commented Jun 4, 2022

Hi there! I was wondering whether anyone else is experiencing the following warning lately. I have been using this solution for months and I'm generally very happy with it! Thank you for putting it together!

Warning (org-element-cache): org-element--cache: Unregistered buffer modifications detected. Resetting.

It happens building Emacs 29.0.5 with this commit: 4f1e748df208ced08c7cda8f96e6a5638ad14240. It has to do with catching, and I wonder whether the modification of the agenda files needs to happen before, that is, we should modify the agenda files and benefit from the catching. Are we not saving the files and it's causing the issue?

I submitted a bug report on the Org-mode mailing list, let's see if that leads somewhere.

Thanks!
Best,
Quique.

@d12frosted
Copy link
Author

@Qkessler apologies, missed your comment. Just found it buried in my emails. I also notice this issue, and still have no remedy. I suspect that it has something to do with the following line.

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

Will share any findings here.

I have been using this solution for months and I'm generally very happy with it! Thank you for putting it together!

Glad to hear that! 🙃

@truemped
Copy link

Hey, I'm running into the same issue. Any insights? I've seen the bug-reports, but they also don't seem updated.

And yes, also using your solution for a while now and I'm super happy with it! Thank you ❤️

@d12frosted
Copy link
Author

@truemped if you are asking about unregistered modifications, then I haven't worked on it yet. Will report here once I figure that out.

@nicolas-graves
Copy link

Hi ! Thanks for this amazing piece of code.
I had an issue because all my files in my org-roam-directory were not org-files. I fixed it by changing the function vulpea-buffer-p this way :

(defun vulpea-buffer-p ()
    "Return non-nil if the currently visited buffer is a note."
    (and buffer-file-name
         (eq (buffer-local-value 'major-mode (current-buffer)) 'org-mode)
         (string-prefix-p
          (expand-file-name (file-name-as-directory org-roam-directory))
          (file-name-directory buffer-file-name))))

I don't know if it's standard and should be taken in consideration, your call ;)

@dustincys
Copy link

@Whil-

Hi Whil,

I guess you wanna grab your agenda file just like this.

  (defun +org-notes-agenda-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."
    (org-element-map
        (org-element-parse-buffer 'headline)
        'headline
      (lambda (h)
        (let
            ((todo-type (org-element-property :todo-type h))
             (scheduled (org-element-property :scheduled h))
             (deadline (org-element-property :deadline h)))
          (or (eq todo-type 'todo)
              (and (not (eq todo-type 'done))
                   (or scheduled deadline)))))
      nil 'first-match))

@akashpal-21
Copy link

Hi ! Thanks for this amazing piece of code. I had an issue because all my files in my org-roam-directory were not org-files. I fixed it by changing the function vulpea-buffer-p this way :

(defun vulpea-buffer-p ()
    "Return non-nil if the currently visited buffer is a note."
    (and buffer-file-name
         (eq (buffer-local-value 'major-mode (current-buffer)) 'org-mode)
         (string-prefix-p
          (expand-file-name (file-name-as-directory org-roam-directory))
          (file-name-directory buffer-file-name))))

I don't know if it's standard and should be taken in consideration, your call ;)

Youre correct this code needs to be fixed especially in emacs > 29. It didn't bork in earlier versions of emacs but this will cause an
rx--translate-bounded-repetition: rx ‘**’ range error since this hooks onto find-file indirectly. I also independently found that the root problem was this function.

I went for this approach

 (defun vulpea-buffer-p ()
  "Return non-nil if the currently visited buffer is a note."
  (and buffer-file-name
       (eq major-mode 'org-mode) ; Check if it's an org file
       (string-prefix-p
        (expand-file-name (file-name-as-directory org-roam-directory))
        (file-name-directory buffer-file-name))))

Please fix it! programs such as helm-bibtex have a very hard time opening pdf files inside org-roam-directory for this,

@bmp
Copy link

bmp commented Sep 10, 2024

Thank you for sharing this code! I just had a question, when I currently use this, all the org-roam buffers with the TODO are still opened everytime I check org-agenda. Is there a way to close them automatically when org-agenda is closed?

@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