Org-capture and Org-agenda from anywhere in Linux
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:
- The dedicated frame where we will be calling
org-capture
is identified by its name, by default"***Capture***"
. This can be customized. - I take a
template
argument as I will setup multiple actions in a desktop entry to access each of myorg-capture
templates. - 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 atemplate
.
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:
- The dedicated frame where we will be calling
org-agenda
is identified by its name, by default"***Agenda***"
. This can be customized. - Similar to
template
before, we now pass acommand
argument to select the desiredorg-agenda
view. - 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:
-
The full code I will be showing is also available in my dotfiles GitHub repository, together with my agenda views and capture templates. ↩︎