3

I've read through all the questions here, and searched extensively online.

Since I've started using org-drill, I have a lot of scheduled tasks with the tag :drill:. Instead of having all these tasks show up in my agenda, I just want to show something like Org-Drill: 42 cards today..

I ripped this off org-effectiveness: (defun drill-card-count() "Print a message with the number of todo tasks in the current buffer" (interactive) (save-excursion (goto-char (point-min)) (message "Number of cards: %d" (count-matches ":drill:")))) I can run this in my agenda but of course it doesn't do what I'd like.

To summarize, I'd like to: 1. Filter any tasks tagged drill from my agenda. 2. Display instead the count of those tagged tasks. 3. Otherwise display the agenda as normal.

Thanks for any help!

ETA: Lawlist's comment below is helpful. I've added this, and it hides the tasks tagged :drill:, but doesn't count them. So, 1 and 3 accomplished, but don't know how to insert a task showing the drill items count. (defun my-skip-tag(tag) "Skip entries that are tagged TAG" (let* ((entry-tags (org-get-tags-at (point)))) (if (member tag entry-tags) (progn (outline-next-heading) (point)) nil)))

(setq org-agenda-custom-commands '(("x" "again" ((agenda "" ((org-agenda-overriding-header (drill-card-count)) (org-agenda-skip-function '(my-skip-tag "drill"))))))))

  • Probably the easiest solution will be to just fold them and make them invisible after you count them (or delete them from the agenda view if you won't be needing them again in that particular buffer) -- you can attach your custom function to the `org-agenda-finalize-hook`. You will want to `(require 'org-agenda)` at the beginning of your session before attaching your custom function to the above-mentioned hook. – lawlist Dec 08 '15 at 21:45
  • I think you are right; that's the solution I'm moving towards. The following removes tasks tagged :drill:, but doesn't count them. ``` (defun my-skip-tag(tag) "Skip entries that are tagged TAG" (let* ((entry-tags (org-get-tags-at (point)))) (if (member tag entry-tags) (progn (outline-next-heading) (point)) nil))) (setq org-agenda-custom-commands '(("x" "My agenda without :drill: tags" ((agenda "" ((org-agenda-overriding-header (drill-card-count)) (org-agenda-skip-function '(my-skip-tag "drill")))))))) ``` – user3814579 Dec 08 '15 at 21:51
  • You can fold text like this: `(overlay-put (make-overlay (line-beginning-position) (line-end-position)) 'invisible t)` Example sets beginning point at the beginning of the line and end point at the end of the line. You'll just need to define a few statements to calculate the beginning and ending of each region as you do your search -- `(while (re-search-forward . . .` or `(while (re-search-backward . . .` depending upon where you are in the buffer when you commence the search. – lawlist Dec 08 '15 at 21:55
  • I don't recommend that you attempt to count them and then skip them and then populate the org-agenda buffer. If you really want to get fancy and understand how to examine text properties of the underlying data that is being gathered as the org-agenda buffer is being populated and modify the data in mid-stream, I can give you some links -- but very few people are that motivated :) My suggestion is that you populate the buffer completely, count your stuff, and then hide or delete what you don't want to see, and then insert your count wherever you want it to appear. – lawlist Dec 08 '15 at 21:58
  • You could also do your count ahead of time, store the information in a temporary variable, and then skip them during the org-agenda data gathering process, and insert the total in the org-agenda buffer. Basically, you'd be doing two searches. And, I suppose you could also put in a counter into the skip function and when a match is found, add a +-one to the count -- storing the information in a temporary variable. I guess there are many ways to accomplish your goal, so don't let me discourage you if you really want to count them and skip them and then populate the org-agenda buffer. – lawlist Dec 08 '15 at 22:08
  • I don't know that much elisp, to tell you the truth. So I'd be willing to make the text invisible, but the clunkier count-twice method seems a little simpler. So I'm trying to slap that together right now. I'd be interested in seeing those links about mid-stream data, just for kicks. :-) – user3814579 Dec 08 '15 at 22:21
  • The following links were tested using `org-agenda-list` to commence the search: http://emacs.stackexchange.com/a/17903/2287 and http://emacs.stackexchange.com/a/12563/2287 The latter just gathers the data, whereas the first one demonstrates how to examine data midstream before the agenda-buffer is populated. The latter can be adapted to use the same principle. My recollection is that `org-agenda-finalize-entries` is used by all three of the main searching functions -- i.e., `org-agenda-list` | `org-tags-view` | `org-search-view` -- but it's been awhile since I wrapped my head around them. – lawlist Dec 08 '15 at 22:32
  • To get an overview of the concept mentioned in the first of the previous links, simply place your cursor on a heading in the agenda buffer and type: `M-x eval-expression RET (text-properties-at (point)) RET` To see everything, you can print it to the `*Messages*` buffer or change the variable responsible for truncating the expression evaluation result to see more of the output. It's basically a long list of categories with corresponding values for several components, which can be extracted with tools such as `get-text-property`. – lawlist Dec 08 '15 at 22:44

1 Answers1

1

As it happens, I just implemented something useful for this for myself before I came across your question. To avoid having all of the source code just in a link, it's also at the bottom of this answer.

To use this, you would construct an empty agenda block like so:

'("x" "Example block agenda"
   ((tags
     "drill"
     ((org-agenda-overriding-header
       (format "Org-Drill: %s cards today."
               (org-agenda-count "drill")))
      (org-agenda-max-entries 0)))

This block would contain just the block header, with a count of all :drill: tasks. In reality, you would probably want an agenda-type block with an appropriate skip function so you only pick up the scheduled cards, but the principle is the same. And of course you'd have other blocks where you'd filter out the drill tasks.

The way this works is as follows:

  1. When the function org-agenda-count is called, it sets up the rest of the process using hooks and advice, then returns a proxy string (by default for this example, "##drill##"), which is incorporated into the header.

  2. During agenda construction, the function org-agenda-finalize-entries is called. Its first argument is a list with one item per agenda entry. We run advice here that counts that list and stores the result.

  3. At some point in here, org-agenda-max-entries is applied and all of the actual entries are discarded.

  4. Finally, org-agenda-finalize-hook is run, along with our hook function that looks for the proxy string from step 1 and replaces it with the appropriate count, then removes all of the advice and so forth.

You should note that the code has one non-Org dependency, a macro called alist-put (which is essentially an alist-only backport of map-put from map.el for use in Emacs 24). Its source code is elsewhere in the repository linked above and included in the code block below, or you can replace it with whatever similar idiom you desire.

Full source code:

(require 'cl-lib)

(defun alist--put (alist key value &optional test)
  "Subroutine used by `alist-put'.

As `alist-put', except back-assignment may be necessary, as with
`delete'."
  (let* ((test  (or test #'eql))
         (elt   (cl-assoc key alist :test test)))
    (if elt (setf (cdr elt) value)
      (setq alist (push (cons key value) alist)))
    alist))

(defmacro alist-put (alist key value &optional test)
  "Associate KEY with VALUE in ALIST.  Return ALIST.

If ALIST does not already contain an association for KEY, it is
added; otherwise, the existing association is updated.

If the optional parameter TEST is supplied, it is used in place
of `eql' to compare elements.

Here, ALIST may be any generalized variable containing an alist."
  (declare (debug (gv-place form form &optional function-form)))
  `(setf ,alist (alist--put ,alist ,key ,value ,test)))

(defcustom org-agenda-count-delimiter "##"
  "String used to delimit the proxy for `org-agenda-count'."
  :group 'org-agenda
  :type  'string)

(defvar org-agenda-count--alist nil
  "Alist containing agenda block counts for `org-agenda-count'.")

(defvar org-agenda-count--block nil
  "Agenda block being counted for `org-agenda-count'.")

(defun org-agenda-count (&optional block)
  "Display a count of the number of items in this agenda block.

For use in custom agenda commands, specifically with
`org-agenda-overriding-header'.  To use this function, call it
while constructing the value for `org-agenda-overriding-header'
and use the return value as if it were the number of items in
this block (before filtering by `org-agenda-max-entries' and
similar).

The optional BLOCK parameter identifies the agenda block being
counted.  To count multiple blocks in the same agenda, each
invocation of `org-agenda-count' should have a different BLOCK
value.

The actual return value is a proxy string incorporating the BLOCK
parameter.  This string is replaced with the actual value during
agenda construction."
  ;; Set up replacement process
  (setq org-agenda-count--block (or block ""))
  (alist-put org-agenda-count--alist
             org-agenda-count--block
             0
             #'equal)
  (advice-add 'org-agenda-finalize-entries :before #'org-agenda--count)
  (add-hook 'org-agenda-finalize-hook #'org-agenda-count--replace)
  ;; Return proxy value
  (concat org-agenda-count-delimiter
          org-agenda-count--block
          org-agenda-count-delimiter))

(defun org-agenda--count (list &optional type)
  "Count the number of entries in this block for `org-agenda-count'.

Intended as (temporary) :before advice for the function
`org-agenda-finalize-hook'."
  (alist-put org-agenda-count--alist
             org-agenda-count--block
             (length list)
             #'equal))

(defun org-agenda-count--regexp ()
  "Return a regexp matching `org-agenda-count' proxy values."
  (let ((delim (regexp-quote org-agenda-count-delimiter)))
    (concat delim "\\([^\n]*?\\)" delim)))

(defun org-agenda-count--reset ()
  "Reset state variables used by `org-agenda-count'.

This includes removing hook functions and temporary advice
functions."
  (setq org-agenda-count--alist nil
        org-agenda-count--block  nil)
  (advice-remove 'org-agenda-finalize-entries #'org-agenda--count)
  (remove-hook 'org-agenda-finalize-hook #'org-agenda-count--replace))

(defun org-agenda-count--replace ()
  "Replace `org-agenda-count' proxy values with entry counts.

Intended for use in `org-agenda-finalize-hook'."
  (save-excursion
    (goto-char (point-min))
    (while (re-search-forward (org-agenda-count--regexp) nil :noerror)
      (let* ((block  (match-string 1))
             (count  (cdr (assoc block org-agenda-count--alist))))
        (when count (replace-match (format "%d" count)))))
    (org-agenda-count--reset)))
Aaron Harris
  • 2,664
  • 17
  • 22