tomasfarias.dev

Org-capture and Org-agenda from anywhere in Linux

· [Tomás Farías Santana]

I use Org for capturing TODO items in an inbox.org file that gets read and displayed in my Org agenda views. A pretty standard workflow, at least for your average Emacs/Org user. However, every time I am not in my Emacs workspace I feel slightly annoyed with the added friction of having to either open a new frame or switch to my Emacs workspace in order to run org-capture or org-agenda. I wish Emacs functions could be called as shell commands that I can bind or add to my launcher.

Luckily for me, they can: With the only prerequisite being that Emacs is running as a daemon, we can use emacsclient to evaluate any ELisp expression, including calling org-capture and org-agenda. I got this idea from this Mac owner’s Club blog post: The author opened my eyes to this new “Emacs functions as shell commands” world that I partially knew it had to exist, but I was missing that last push to reach the light at the end of the tunnel.

Here, I will show you how I have set up my own ELisp functions to run on Linux to achieve access to org-capture and org-agenda from anywhere1.

Show me the ELisp

Enough introduction, let’s jump right in. First, this is the ELisp function I use to invoke org-capture:

 1(defcustom tomas/org-capture-frame-name "**Capture**"
 2  "Customize dedicated frame name to launch `org-capture' in."
 3  :type 'string)
 4
 5(defun tomas/org-capture-frame (template)
 6  "Invoke `org-capture' in a dedicated Emacs frame.
 7
 8     This function is designed to be called from a shell script using `emacsclient'.
 9     If the dedicated frame already exists, we will use it, otherwise we will create a
10     new frame.
11
12     Finally, the dedicated frame will be deleted up after `org-capture' finalizes."
13  (interactive '(nil))
14
15  (if (not (equal tomas/org-capture-frame-name (frame-parameter nil 'name)))
16      (make-frame '((name . tomas/org-capture-frame-name))))
17
18  (select-frame-by-name tomas/org-capture-frame-name)
19  (delete-other-windows)
20
21  (defun org-capture-after-finalize-clean-up ()
22    "Clean up after `org-capture' finalizes.
23
24    We delete the dedicated frame and removing advice."
25    (advice-remove 'org-capture-place-template 'delete-other-windows)
26    (remove-hook 'org-capture-after-finalize-hook 'org-capture-after-finalize-clean-up)
27
28    (select-frame-by-name tomas/org-capture-frame-name)
29    (delete-frame nil t))
30
31  (add-hook 'org-capture-after-finalize-hook 'org-capture-after-finalize-clean-up)
32  (advice-add #'org-capture-place-template :after 'delete-other-windows)
33
34  (org-capture nil template))

A few things to note in this function:

  1. The dedicated frame where we will be calling org-capture is identified by its name, by default "***Capture***". This can be customized.
  2. I take a template argument as I will setup multiple actions in a desktop entry to access each of my org-capture templates.
  3. There is some adding and removing of a hook and function advice necessary to have org-capture occupy the entire frame. This partially works, but a more fluent Emacs user may find a better way to achieve the same result. Moreover, as I will show you later, it doesn’t quite work if not passing a template.

Similarly, here is the ELisp function used to invoke org-agenda:

 1(defcustom tomas/org-agenda-frame-name "**Agenda**"
 2  "Customize dedicated frame name to launch `org-agenda' in."
 3  :type 'string)
 4
 5(defun tomas/org-agenda-frame (command)
 6  "Invoke `org-agenda' in a dedicated Emacs frame.
 7
 8   This function is designed to be called from a shell script using `emacsclient'.
 9   If the dedicated frame already exists, we will use it, otherwise we will create a
10   new frame.
11
12   Finally, the dedicated frame will be deleted up after `org-agenda' finalizes."
13  (interactive '(nil))
14
15  (if (not (equal tomas/org-agenda-frame-name (frame-parameter nil 'name)))
16      (make-frame '((name . tomas/org-agenda-frame-name))))
17
18  (select-frame-by-name tomas/org-agenda-frame-name)
19  (delete-other-windows)
20
21  (defun org-agenda-quit--clean-up ()
22    "Close the frame after `org-agenda-quit'."
23    (advice-remove 'org-agenda 'delete-other-windows)
24    (advice-remove 'org-agenda-quit 'org-agenda-quit--clean-up)
25    (advice-remove 'org-agenda-Quit 'org-agenda-quit--clean-up)
26
27    (select-frame-by-name tomas/org-agenda-frame-name)
28    (delete-frame nil t))
29
30  (advice-add 'org-agenda-quit :after #'org-agenda-quit--clean-up)
31  (advice-add 'org-agenda-Quit :after #'org-agenda-quit--clean-up)
32  (advice-add 'org-agenda :after #'delete-other-windows)
33
34  (org-agenda nil command))

Again, a few of things to note here:

  1. The dedicated frame where we will be calling org-agenda is identified by its name, by default "***Agenda***". This can be customized.
  2. Similar to template before, we now pass a command argument to select the desired org-agenda view.
  3. The clean-up code, which is very similar in objectives and limitations as the previous function, is tied up to quitting the agenda as there is nothing finalizing here.

Emacs functions as desktop entries

With the functions loaded, we can now add desktop entries to call them via emacsclient. Note that, as stated at the beginning, using emacsclient will require Emacs to be running as a daemon.

Here is the desktop entry for org-capture:

 1[Desktop Entry]
 2Name=Capture
 3Comment=Capture in org-mode using a separate Emacs frame
 4Exec=/usr/bin/emacsclient -c -e '(tomas/org-capture-frame nil)' -F '((name . "**Capture**"))'
 5Icon=emacs
 6Type=Application
 7Terminal=false
 8Categories=TextEditor;
 9Actions=inbox;
10
11[Desktop Action inbox]
12Name=Inbox
13Exec=/usr/bin/emacsclient -c -e '(tomas/org-capture-frame "i")' -F '((name . "**Capture**"))'
14
15[Desktop Action inbox]
16Name=Work Inbox
17Exec=/usr/bin/emacsclient -c -e '(tomas/org-capture-frame "wi")' -F '((name . "**Capture**"))'

Note that we have defined two actions: one for each template. My application launcher will display multiple options for “Capture”: one for each capture template.

And similarly, here is the desktop entry for org-agenda:

 1[Desktop Entry]
 2Name=Agenda
 3Comment=Agenda in org-mode using a separate Emacs frame
 4Exec=/usr/bin/emacsclient -c -e '(tomas/org-agenda-frame nil)' -F '((name . "**Agenda**"))'
 5Icon=emacs
 6Type=Application
 7Terminal=false
 8Categories=TextEditor;
 9Actions=inbox;
10
11[Desktop Action inbox]
12Name=All
13Exec=/usr/bin/emacsclient -c -e '(tomas/org-agenda-frame "A")' -F '((name . "**Agenda**"))'
14
15[Desktop Action inbox]
16Name=Work
17Exec=/usr/bin/emacsclient -c -e '(tomas/org-agenda-frame "w")' -F '((name . "**Agenda**"))'

Note again that with the two actions defined my application launcher will display multiple options for “Agenda”: one for each agenda view.

I am placing both of these in the $HOME/.local/share/applications/ directory.

See it in action

With the See me open org-capture from my application launcher2, capture a todo item, and check it on my agenda:


  1. The full code I will be showing is also available in my dotfiles GitHub repository, together with my agenda views and capture templates. ↩︎

  2. I am using fuzzel↩︎

Reply to this post by email ↪