Publishing the org-mode agenda
updated
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 thedate
argument (more on that on the No description for this link).- render-agenda.el cannot work on it's own, since it contains placeholders for sed.
- The
__start_date__
can accept any strings acceptable byorg-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 returntoday
so basicallyargs[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!