9

Q: how can I control where the org todo keywords buffer appears?

Entering a todo keyword with C-c C-t (org-todo) opens a new buffer with the keyword options and then closes it again after I select one. So far, so good. However, it takes over another window to do so, which is less good, especially since it really only needs to display a line or two with the keywords.

So, with the following layout, hitting C-c C-t while in the left window (some-org-buffer) will open *Org todo* in the right window:

+---------------------+---------------------+
|                     |                     |
|                     |                     |
|                     |                     |
|                     |                     |
|   some-org-buffer   |  some-other-buffer  |
|                     |                     |
|                     |                     |
|                     |                     |
|                     |                     |
+---------------------+---------------------+

Instead, I would like to have a small window pop up as a vertical split, as below:

+---------------------+---------------------+
|                     |                     |
|                     |                     |
|   some-org-buffer   |  some-other-buffer  |
|                     |                     |
|                     |                     |
+---------------------+                     |
|                     |                     |
|     *Org todo*      |                     |
|                     |                     |
+---------------------+---------------------+

Cribbing from this answer, I wrote a function to put in the display-buffer-alist:

(defun org-todo-position (buffer alist)
  (let ((win (car (cl-delete-if-not
                   (lambda (window)
                     (with-current-buffer (window-buffer window)
                       (memq major-mode
                             '(org-mode org-agenda-mode))))
                   (window-list)))))
    (when win
      (let ((new (split-window win -5 'below)))
        (set-window-buffer new buffer)
        new))))

(add-to-list 'display-buffer-alist
             (list " \\*Org todo\\*" #'dan-org-todo-position))

However, that fails to work. Sigh. What have I done wrong with the display-buffer-alist? More to the point, how do I get my todo keyword buffer to pop up where I want it?

Dan
  • 32,584
  • 6
  • 98
  • 168
  • Not an answer to your question, but you may wish to consider modifying `org-switch-to-buffer-other-window` to do what you want -- you can create a condition that does whatever you want. – lawlist Aug 17 '15 at 20:31
  • @lawlist: thanks, I dug around and found out about `org-switch-to-buffer-other-window` and a whole bunch of other ugly `org` innards. See answer for the ignominious "solution." – Dan Aug 17 '15 at 20:38
  • While incorporating all of this into my .emacs, I took a close look at your positioning function, and I feel like I'm missing something regarding your definition of `win`. Is there a reason you can't just use `(selected-window)` here? – Aaron Harris Oct 06 '15 at 15:36
  • @AaronHarris: haven't tried it; I had (lightly) adapted an answer from a different post that used this mechanism. Give it a shot and see if it works. – Dan Oct 06 '15 at 17:21
  • @Dan: Yep, it works. I was just a little worried that some corner-case situation would cause it to blow up. And thanks for that function, btw. It does exactly what I want it to do, and I don't think I would have been able to articulate that that's what I wanted before seeing this question. – Aaron Harris Oct 06 '15 at 18:09
  • @AaronHarris: glad it works, and also glad the the function is helpful. I'd also gone a long time getting frustrated by the `todo` positioning before I finally figured out what would fit my head better. – Dan Oct 06 '15 at 18:12

4 Answers4

3

Sigh. I found a "solution," but it's an ugly one that requires overwriting an existing org function. I would prefer one that does not require modifying the org source code, but am putting the results of my archaeological dig here in case anyone else can use it.

As I've noticed in the past with other org functions, org-mode is rather opinionated about how it handles windows.

Buried deep in the source code of org-todo is a call to the function org-fast-todo-selection. That function, in turn, calls org-switch-to-buffer-other-window, whose docstring reads, in part:

Switch to buffer in a second window on the current frame. In particular, do not allow pop-up frames.

Uh oh.

Okay, I'll bite: let's look at org-switch-to-buffer-other-window. It uses the org-no-popups macro. Its docstring reads, in part:

Suppress popup windows. Let-bind some variables to nil around BODY...

It turns out that display-buffer-alist is one of the variables let-bound to nil.

(Head explodes.)

Ultimately, we can get it not to ignore the display-buffer-alist by editing org-fast-todo-selection and replacing the org-switch-to-buffer-other-window line with plain old switch-to-buffer-other-window:

(defun org-fast-todo-selection ()
  "Fast TODO keyword selection with single keys.
Returns the new TODO keyword, or nil if no state change should occur."
  (let* ((fulltable org-todo-key-alist)
     (done-keywords org-done-keywords) ;; needed for the faces.
     (maxlen (apply 'max (mapcar
                  (lambda (x)
                (if (stringp (car x)) (string-width (car x)) 0))
                  fulltable)))
     (expert nil)
     (fwidth (+ maxlen 3 1 3))
     (ncol (/ (- (window-width) 4) fwidth))
     tg cnt e c tbl
     groups ingroup)
    (save-excursion
      (save-window-excursion
    (if expert
        (set-buffer (get-buffer-create " *Org todo*"))
      ;; (org-switch-to-buffer-other-window (get-buffer-create " *Org todo*")))
      (switch-to-buffer-other-window (get-buffer-create " *Org todo*")))
    (erase-buffer)
    (org-set-local 'org-done-keywords done-keywords)
    (setq tbl fulltable cnt 0)
    (while (setq e (pop tbl))
      (cond
       ((equal e '(:startgroup))
        (push '() groups) (setq ingroup t)
        (when (not (= cnt 0))
          (setq cnt 0)
          (insert "\n"))
        (insert "{ "))
       ((equal e '(:endgroup))
        (setq ingroup nil cnt 0)
        (insert "}\n"))
       ((equal e '(:newline))
        (when (not (= cnt 0))
          (setq cnt 0)
          (insert "\n")
          (setq e (car tbl))
          (while (equal (car tbl) '(:newline))
        (insert "\n")
        (setq tbl (cdr tbl)))))
       (t
        (setq tg (car e) c (cdr e))
        (if ingroup (push tg (car groups)))
        (setq tg (org-add-props tg nil 'face
                    (org-get-todo-face tg)))
        (if (and (= cnt 0) (not ingroup)) (insert "  "))
        (insert "[" c "] " tg (make-string
                   (- fwidth 4 (length tg)) ?\ ))
        (when (= (setq cnt (1+ cnt)) ncol)
          (insert "\n")
          (if ingroup (insert "  "))
          (setq cnt 0)))))
    (insert "\n")
    (goto-char (point-min))
    (if (not expert) (org-fit-window-to-buffer))
    (message "[a-z..]:Set [SPC]:clear")
    (setq c (let ((inhibit-quit t)) (read-char-exclusive)))
    (cond
     ((or (= c ?\C-g)
          (and (= c ?q) (not (rassoc c fulltable))))
      (setq quit-flag t))
     ((= c ?\ ) nil)
     ((setq e (rassoc c fulltable) tg (car e))
      tg)
     (t (setq quit-flag t)))))))
Dan
  • 32,584
  • 6
  • 98
  • 168
  • Could you use cl `flet` here to rebind `org-switch-to-buffer-other-window`? Wondering if you can advise or otherwise wrap `org-fast-todo-selection` rather than overwriting it. – glucas Aug 18 '15 at 00:19
  • @glucas: good idea. Tried it and couldn't get it to work, though. – Dan Aug 18 '15 at 11:30
  • 1
    @Dan, I was able to get this to work by redefining `org-switch-to-buffer-other-window` to just `(switch-to-buffer-other-window args)`. I also removed `&rest` from the function arguments. – sk8ingdom Jul 27 '16 at 00:23
3

I recently figured out how to make Org-mode capture in a new frame. Modifying that code to use your function is pretty straightforward. Here's how I'd do it:

(defun my/org-todo-window-advice (orig-fn)
  "Advice to fix window placement in `org-fast-todo-selection'."
  (let  ((override '("\\*Org todo\\*" dan-org-todo-position)))
    (add-to-list 'display-buffer-alist override)
    (my/with-advice
        ((#'org-switch-to-buffer-other-window :override #'pop-to-buffer))
      (unwind-protect (funcall orig-fn)
        (setq display-buffer-alist
              (delete override display-buffer-alist))))))

(advice-add #'org-fast-todo-selection :around #'my/org-todo-window-advice)

For completeness' sake, here's the definition of the my/with-advice macro from the other answer:

(defmacro my/with-advice (adlist &rest body)
  "Execute BODY with temporary advice in ADLIST.

Each element of ADLIST should be a list of the form
  (SYMBOL WHERE FUNCTION [PROPS])
suitable for passing to `advice-add'.  The BODY is wrapped in an
`unwind-protect' form, so the advice will be removed even in the
event of an error or nonlocal exit."
  (declare (debug ((&rest (&rest form)) body))
           (indent 1))
  `(progn
     ,@(mapcar (lambda (adform)
                 (cons 'advice-add adform))
               adlist)
     (unwind-protect (progn ,@body)
       ,@(mapcar (lambda (adform)
                   `(advice-remove ,(car adform) ,(nth 2 adform)))
                 adlist))))
Aaron Harris
  • 2,664
  • 17
  • 22
3

Not related to fixing the buffer location but you can make the todo selection appear in the smaller minibuffer via

(setq org-use-fast-todo-selection 'expert)

The default 'auto value displays the selection in a *Org todo* buffer (when todo keywords have keys defined) which imo takes up too much space.

Kevin
  • 76
  • 5
2

Looking in someone else's config, I found a way to control where *Org Src.* and *Org todo* buffers appear. Now when I hit C-c C-t or C-c ' these buffers display in a new window at the bottom of my current window layout, and selecting a TODO state or hitting C-c ' (org-edit-src-exit) returns my window configuration to its original layout.

This solution involves using shackle and some Elisp:

Step 1

Download shackle from MELPA. Here is how I set it up with use-package in my init file:

(use-package shackle
    :ensure t
    :diminish shackle-mode  ; hide name in mode-line
    :config
    (setq shackle-rules
          '(("\\*Org Src.*" :regexp t :align below :select t)
            (" *Org todo*" :align below :select t)))
    (shackle-mode t))

The important settings here are:

(setq shackle-rules
              '(("\\*Org Src.*" :regexp t :align below :select t)
                (" *Org todo*" :align below :select t)))

Source

Note especially the space between " and *Org on the bottom line. I haven't tested setting :align to other locations, but shackle seems to be pretty flexible, so it's worth reading the documentation and experimenting with it.

Step 2

Now we make org-mode listen to shackle with some more Elisp in our init file. To get *Org Src.* buffers to follow shackle-rules:

(setq org-src-window-setup 'other-window)

Source

Unfortunately org-mode doesn't provide an analogous setting for *Org todo* buffers. This hack makes up for that (and probably makes (setq org-src-window-setup 'other-window) superfluous, but someone more knowledgeable will have to chime in):

;; Re-define org-switch-to-buffer-other-window to NOT use org-no-popups.
;; Primarily for compatibility with shackle.
(defun org-switch-to-buffer-other-window (args)
  "Switch to buffer in a second window on the current frame.
In particular, do not allow pop-up frames.
Returns the newly created buffer.
Redefined to allow pop-up windows."
  ;;  (org-no-popups
  ;;     (apply 'switch-to-buffer-other-window args)))
  (switch-to-buffer-other-window args))

Source

tirocinium
  • 781
  • 5
  • 8