grm blog. Work is copyrighted unless otherwise stated.
2022-02-20 Sun
^

Publishing the org-mode agenda

updated <2023-12-08 Fri>

In this here blog post I describe how I managed to get my agenda view straight out of emacs and into a web page so I can access it from everywhere.

It is hackish and flimsy but it's been working for a few months years now, so here goes nothing.

Purpose (specifications)

  • I want the agenda view just how it is in emacs.
  • I don't want any interactivity, just an up to date agenda of today and the next few days.
  • Possibly control the starting day of the agenda span, not just today.
  • Run emacs on the server on every single request (!).

I'm proud to say that all of the above are implemeted.

Prerequisites

For this to work the source of truth for my org agenda files should be in a server. Specifically I have a small webDAV share of a directory that contains my org files.

This means I can access them from any emacs in the world (all hail TRAMP 1), and it's just a couple lines of elisp to make org mode in any emacs work with my files:

(setq org-agenda-files '("/davs:user@host:/path/to/dir/"))
(setq org-directory "/davs:user@host:/path/to/dir/")
;; I also sync my archive file
(setq org-archive-location (concat org-directory "archive.org::datetree/"))

The webDAV password can be saved in ~/.authinfo or encrypted with pgp to automate the login but you can also just type the password.

Oh, webDAV also works with orgzly which is how I edit/add notes/tasks from my phone.

nginx webDAV

For completness here is the nginx webDAV configuration.

It's just the following directives inside a location {} block

root   /var/www/dav;

dav_methods PUT DELETE MKCOL COPY MOVE;
dav_ext_methods PROPFIND OPTIONS;

# Adjust as desired:
dav_access user:rw group:rw all:r;
client_max_body_size 0;
create_full_put_path on;
client_body_temp_path /var/www/client-temp;
autoindex on;

auth_basic "dav message";
auth_basic_user_file /etc/nginx/htpasswd-dav;

Components

nginx
a nice web server
emacs
a good editor
org-mode
a fun markup language
lua
wait what?
sed
I'm sure you can do it better.

These are the components used for this to work. The next paragraph will explain what everyone's role is.

Basic idea

The first thing I tried to do was get the output of a command (like e.g. ls -la) to be returned as an answer from nginx upon curl'ing my specific endpoint.

There are some ways to do this with nginx, but the most flexible I found was a lua plugin for nginx namely ngx_http_lua_module. Apparently this lets your run arbitrary lua code using content_by_lua_block { ...lua code... } inside a location. Horrendous!

So the next step was to create a command that would return my agenda in the stdout, so I can then access it in lua and "return" it as a response to the requester.

Turns out emacs has a function to write the rendered agenda to a file but not to stdout, so I had to wrap stuff in a script.

After this was in place, everything else was beautifications on top of it and UX optimisations.

Implementation

bash

The bash part agenda-to-html.sh starts by sedding a couple of variables in the render-agenda.el file, the date (that we can get from the uri args) as well as the span which defaults to 15 days (and is currently not dynamic)

Then it calls emacs with this file and produces the htmlized agenda buffer.

Then, using sed once again, it adds some nav stuff to the resutling html, as well as making the dates clickable (hrefs).

It then cats the file to stdout and cleans the generated stuff.

#!/bin/bash

DATE="${1:-today}"
SPAN=15

# Generate agenda
echo "[$(date -uIseconds)] Generating agenda [$DATE $SPAN]" >&2
sed -e "s#__start_date__#${DATE}#" -e "s#__span__#${SPAN}#" /etc/emacs/render-agenda.el > /tmp/rend.el
emacs -q --no-site-file -nsl --script /tmp/rend.el > /dev/null

# Add org-read-date text area and nav # %2B is + in urlencoding
sed -i 's#<body>#</body>\
<span class="org-agenda-date">\
<a href="/agenda?date=-1m">-1m</a>\
<a href="/agenda?date=-1w">-1w</a>\
<a href="/agenda?date=today">today</a>\
<a href="/agenda?date=%2B1w">+1w</a>\
<a href="/agenda?date=%2B1m">+1m</a>\
</span>\
<form action="/agenda">\
<label for="start_date"></label>\
<input type="text" name="date" id="start_date"> <input type="submit" value="org-read-date">\
</form>\
<small>(<a href="https://orgmode.org/manual/The-date_002ftime-prompt.html">help</a>)</small>#' /tmp/result.html

# href the days
sed -i 's#<span class="org-agenda-date">\(.*\) - \(.*\)</span>#<span class="org-agenda-date"><a href="/agenda?date=\1">\1 - \2</a></span>#' /tmp/result.html
sed -i 's#<span class="org-agenda-date-weekend">\(.*\) - \(.*\)</span>#<span class="org-agenda-date-weekend"><a href="/agenda?date=\1">\1 - \2</a></span>#' /tmp/result.html

cat /tmp/result.html

# Cleanup
rm /tmp/result.html
rm /tmp/rend.el

emacs

In the emacs side of things, the two files used are the render-agenda.el one as well as the actual config gtd-config.el. The config file is loaded first and sets some defaults like our theme, the location of the org agenda files and last but not least it defines a new custom command in the org-agenda-custom-commands which specifies the location of the written file as well as it's type (html). This is done in render-agenda.el by the org-batch-store-agenda-views command.

Important stuff to note:

  • org-agenda-format-date "%Y-%m-%d - %A %d %B" is used by agenda-to-html.sh to make the days clickable, the first part (%Y-%m-%d) is what is passed to the date argument (more on that on the 1).
  • render-agenda.el cannot work on it's own, since it contains placeholders for sed.
  • The __start_date__ can accept any strings acceptable by org-read-date and is what makes the UX of this whole project worth it. org-read-date manual
  • in gtd-config.el I'm directly using the dav files shared by nginx since they both run on the same server. Using TRAMP here would make things way more sluggish.
(load-file "/etc/emacs/gtd-config.el")

;; clone htmlize somehwere!
(load-file "/etc/emacs/emacs-htmlize/htmlize.el")
(require 'htmlize)

(org-batch-store-agenda-views
 org-agenda-format-date "%Y-%m-%d - %A %d %B"
 org-agenda-start-day "__start_date__"
 org-agenda-span __span__
 )
(load-theme 'tsdh-dark t)

(setq org-agenda-files '("/var/www/dav/dav/"))
(setq org-agenda-include-diary nil)
(setq org-agenda-time-grid nil)

(setq org-directory "/var/www/dav/dav/")
(setq org-agenda-hide-tags-regexp "hide\\|hidden")

(setq org-archive-location (concat org-directory "archive.org::datetree/"))

;; Custom theming
;; (require 'org)
;; (set-face-attribute 'org-agenda-date nil
;;                  :foreground "Green"
;;                  :weight 'bold)

(setq org-agenda-custom-commands
      '(("X" agenda ""
         nil
         ("/tmp/result.html"))))

nginx

Fianlly the thing that puts it all togheter.

Note:

  • Set default_type to html
  • DEFINITELY use basic auth (see Security)
  • see this beautiful line of code next to io.popen? What I wanted was a ternary operator to check if date exists in the args table, and if not return today so basically args[date] ? args[date] : "today" in pseudocode. This was the day I learnd that lua doesn't have such a thing.

The popen command runs our script with a positional argument of "today" or whatever it finds in the uri (for example https://my.host.com/agenda?date=<DATE>

location /agenda {
    default_type text/html;
    auth_basic "agendav";
    auth_basic_user_file /etc/nginx/htpasswd-dav;
    content_by_lua_block {
        local args = ngx.req.get_uri_args()
        local handle = io.popen("etc/emacs/agenda-to-html.sh '" .. (function() if args["date"] == nil then return "today" else return args["date"] end end)() .. "'" )
        local res = handle:read("*a")
        handle:close()
        ngx.print(res)
    }
}

Security

From a sec standpoint, this doesn't seem very vulnerable so long as you make sure that the only input (date) can't be misused by a malicious actor, either in the lua code, or in the lisp.

Also, I consider basic auth a must have because each and every request is creating an emacs process that does all this, which means if your endpoint is open, a script kiddie could ddos you by simply doing many requests consecutively, which will start many emacses and who knows what will happen.

Screenshots

Here is a screenshot of an empty week!

emacs-agenda-online.png

Footnotes:

1

Not Trump, fuck this guy.