I like the way Org Mode lets me logically group
functions into headings. If I give the heading a
CUSTOM_ID property (which is also handy for
exporting to HTML, as it turns into an link
anchor), I can use that property to find the
subtree. Then I can use
org-babel-execute-subtree to execute all source
blocks in that subtree, which means I can mix
scripting languages if I want to.
Here's the code:
(defunmy-org-execute-subtree-by-custom-id (id &optional filename)
"Prompt for a CUSTOM_ID value and execute the subtree with that ID.If called with \\[universal-argument], prompt for a file, and then prompt for the ID."
(interactive (if current-prefix-arg
(let ((file (read-file-name "Filename: ")))
(list
(with-current-buffer (find-file-noselect file)
(completing-read
"Custom ID: "
(org-property-values "CUSTOM_ID")))
file))
(list
(completing-read "Custom ID: " (org-property-values "CUSTOM_ID")))))
(with-current-buffer (if filename (find-file-noselect filename) (current-buffer))
(let ((pos (org-find-property "CUSTOM_ID" id)))
(if pos
(org-babel-execute-subtree)
(if filename(error"Could not find %s in %s" id filename)
(error"Could not find %s" id))))))
Technical notes: org-babel-execute-subtree
narrows to the current subtree, so if I want
anything from the rest of the buffer, I need to
widen the focus again. Also, it's wrapped in a
save-restriction and a save-excursion, so
someday I might want to figure out how to handle
the cases where I want to change what I'm looking
at.
elisp: links in Org Mode let me call functions
by clicking on them or following them with C-c
C-o (org-open-at-point). This means I can make
links that execute subtrees that might even be in
a different file. For example, I can define links
like these:
: Tweaked script to report when the server is already downscaled
: Add a check to see if the meetings are still running
Times and time zones trip me up. Even with
calendar notifications, I still fumble scheduled
events. Automation helps me avoid embarrassing
hiccups.
We run BigBlueButton as a self-hosted web
conferencing server for EmacsConf. It needs at
least 8 GB of RAM when active. When it's dormant,
it fits on a 1 GB RAM virtual private server. It's
easy enough to scale the server up and down as
needed. Using the server for Emacs meetups in
between EmacsConfs gives people a way to get
together, and it also means I can regularly test
the infrastructure. That makes scaling it up for
EmacsConf less nerve-wracking.
I have some code that processes various Emacs
meetup iCalendar files (often with repeating
entries) and combines them into one iCal file that
people can subscribe to calendar, as well as Org files in different timezones that they can include
in their org-agenda-files. The code I use to
parse the iCal seems to handle time zones and
daylight savings time just fine. I set it up so
that the Org files have simple non-repeating
entries, which makes them easy to parse. I can use
the Org file to determine the scheduled jobs to
run with cron on a home server (named xu4) that's
up all the time.
This code parses the Org file for schedule
information, then generates pairs of crontab
entries. The first entry scales the BigBlueButton
server up 1 hour before the event using my
bbb-testing script, and the second entry scales
the server down 6 hours after the event using my
bbb-dormant script (more info). That gives organizers time to
test it before the event starts, and it gives
people plenty of time to chat. A shared CPU 8 GB
RAM Linode costs USD 0.072 per hour, so that's USD
0.50 per meetup hosted.
#!/bin/bashsource ~/.profile
CHECK_SCRIPT="/home/sacha/bin/check_meetings.py"SCRIPT_OUTPUT=$(python3 $CHECK_SCRIPT)
ACTIVE_COUNT=$(echo"$SCRIPT_OUTPUT" | grep -c "Active:")
if echo"$SCRIPT_OUTPUT" | grep -q "Already downsized"; thenecho $(date) "Node already downsized, no action needed." | tee -a /home/sacha/bbb.log
elif [ "$ACTIVE_COUNT" -eq 0 ]; thenecho $(date) "No active meetings found. Okay to put it to sleep..." | tee -a /home/sacha/bbb.log
~/bin/bbb-dormant
elseecho $(date) "Server is busy with $ACTIVE_COUNT active meeting(s)." | tee -a /home/sacha/bbb.log
fi
and that uses this Python file to query the API:
Python script to check if there are ongoing meetings
import hashlib
import requests
import os
from dotenv import load_dotenv
load_dotenv()
SECRET= os.environ['BBB_SECRET']
URL= os.environ['BBB_API_URL']
if SECRET isNoneor URL isNone:
print("Please specify BBB_SECRET and BBB_API_URL")
exit(1)
import xml.etree.ElementTree as ET
defformat_report(text):
root= ET.fromstring(text)
s=""active_meetings= [
m.find('meetingName').text +' ('+ m.find('participantCount').text +')'for m in root.findall('.//meeting')
if m.find('running').text =='true'
]
if active_meetings:
for name in active_meetings:
s+= f"Active: {name}\n"else:
s="None"return s
defget_meetings():
query="getMeetings"checksum= hashlib.sha1((query + SECRET).encode('utf-8')).hexdigest()
full_url= f"{URL}{query}?checksum={checksum}"response= requests.get(full_url)
if'Bad Gateway'in response.text:
print('Already downsized')
else:
print(format_report(response.text).strip())
get_meetings()
Here's bbb-dormant:
#!/bin/bashsource /home/sacha/.profile
PATH=/home/sacha/.local/bin/:$PATHCURRENT_TYPE=$(linode-cli linodes view $BBB_ID --format "type" --text --no-headers 2> /dev/null)
echo"Current node type: $CURRENT_TYPE"if [ "$CURRENT_TYPE" != "g6-nanode-1" ]; thenecho Powering off
linode-cli linodes shutdown $BBB_ID# Wait for the Linode to reach the 'offline' state before resizing
sleep 3m
echo"Waiting for shutdown to complete..."while [ "$(linode-cli linodes view $BBB_ID --format 'status' --text --no-headers)" != "offline" ]; do
sleep 5m
doneecho $(date) "Resizing BBB node to nanode, dormant" | tee -a /home/sacha/bbb.log
linode-cli linodes resize $BBB_ID --type g6-nanode-1 --allow_auto_disk_resize false
sleep 5m
linode-cli linodes boot $BBB_IDelseecho"Already downsized"fi
The code for generating the crontab works because
meetups don't currently overlap. If there were,
I'll need to tweak the code so that the server
isn't downscaled in the middle of a meetup. It'll
be a good problem to have.
I need to load the crontab entries by using
crontab bbb.crontab. Again, I can tell Org Mode
to run this on the xu4 home server. This time I
use the :dir argument to specify the default
directory, like this:
#+begin_src sh :dir "/ssh:xu4:~" :results silentcrontab bbb.crontab#+end_src
Then cron can take care of things automatically,
and I'll just get the e-mail notifications from
Linode telling me that the server has been
resized. This has already come in handy, like when
I thought of Emacs APAC as being on Saturday, but
it was actually on Friday my time.
I have another Emacs Lisp block that I use to
retrieve all the info and update the list of
meetups. I can add (goto-char (org-find-property
"CUSTOM_ID" "crontab")) to find this section and
use org-babel-execute-subtree to execute all the
code blocks. That makes it an automatic part of my
process for updating the Emacs Calendar and Emacs
News. Here's the code that does the calendar part
(Org source):
Hmm. Come to think of it, the technique of "go to
a specific subtree and then execute it" is pretty
powerful. In the past, I've found it handy to
execute source blocks by name. Executing a subtree
by custom ID is even more useful because I can
easily mix source blocks in different languages or
include other information. I think that's worth
adding a my-org-execute-subtree-by-custom-id
function to my Emacs configuration. Combined with
an elisp: link, I can make links that execute
functional blocks that might even be in different
files. That could be a good starting point for a
dashboard.
I love the way Emacs can easily work with files
and scripts in different languages on different
computers, and how it can help me with times and
time zones too. This code should help me avoid
brain hiccups and calendar mixups so that people
can just enjoy getting together. Now I don't have
to worry about whether I remembered to set up cron
entries and if I did the math right for the times.
We'll see how it holds up!
: Limited my-org-cut-subtree-or-list-item to Inbox.org and news.org.
Defining shortcuts in org-speed-commands is
handy because you can use these single-key
shortcuts at the beginning of a subtree. With a
little modification, they'll also work at the
beginning of list items.
I want k to be an org-speed-commands that cuts
the current subtree or list item. This is handy
when I'm cleaning up the Mastodon toots in my
weekly review or getting rid of outline items that
I no longer need. By default, k is mapped to
org-cut-subtree, but it's easy to override.
(defunmy-org-cut-subtree-or-list-item (&optional n)
"Cut current subtree or list item."
(cond; limit this to only Inbox.org and news.org
((not (string-match (regexp-opt '("Inbox.org""news.org"))
(or (buffer-file-name) "")))
(message "Let's only cut things in Inbox.org or news.org")) ; do nothing
((and (looking-at org-outline-regexp) (looking-back "^\**" nil))
(org-cut-subtree n))
((looking-at (org-item-re))
(kill-region (org-beginning-of-item) (org-end-of-item)))))
(with-eval-after-load'org
(setf (alist-get "k" org-speed-commands nil nil #'string=)
#'my-org-cut-subtree-or-list-item))
So now, if I put my cursor before "1." below and press k:
- this
1. is a
- nested
2. list
- with levels
it will turn into:
this
list
with levels
You can find out a little more about Org Mode speed commands in the Org manual: (info "(org) Speed Keys").
One of the things I like about blogging from Org
Mode in Emacs is that it's easy to add properties
to the section that I'm working on and then use
those property values elsewhere. For example, I've
modified Emacs to simplify tooting a link to my
blog post and saving the Mastodon status URL in
the EXPORT_MASTODON property. Then I can use
that in my 11ty static site generation process to
include a link to the Mastodon thread as a comment
option.
First, I need to export the property and include
it in the front matter. I use .11tydata.json files
to store the details for each blog post. I
modified ox-11ty.el so that I could specify
functions to change the front matter
(org-11ty-front-matter-functions,
org-11ty--front-matter):
(defvarorg-11ty-front-matter-functions nil
"Functions to call with the current front matter plist and info.")
(defunorg-11ty--front-matter (info)
"Return front matter for INFO."
(let* ((date (plist-get info :date))
(title (plist-get info :title))
(modified (plist-get info :modified))
(permalink (plist-get info :permalink))
(categories (plist-get info :categories))
(collections (plist-get info :collections))
(extra (if (plist-get info :extra) (json-parse-string
(plist-get info :extra)
:object-type'plist))))
(seq-reduce
(lambda (prev val)
(funcall val prev info))
org-11ty-front-matter-functions
(append
extra
(list :permalink permalink
:date (if (listp date) (car date) date)
:modified (if (listp modified) (car modified) modified)
:title (if (listp title) (car title) title)
:categories (if (stringp categories) (split-string categories) categories)
:tags (if (stringp collections) (split-string collections) collections))))))
Then I added the EXPORT_MASTODON Org property as
part of the front matter. This took a little
figuring out because I needed to pass it as one of
org-export-backend-options, where the parameter
is defined as MASTODON but the actual property
needs to be called EXPORT_MASTODON.
Then I added the Mastodon field as an option to my
comments.cjs shortcode. This was a little tricky
because I'm not sure I'm passing the data
correctly to the shortcode (sometimes it ends up
as item.data, sometimes it's item.data.data,
…?), but with ?., I can just throw all the
possibilities in there and it'll eventually find
the right one.
constpluginRss = require('@11ty/eleventy-plugin-rss');
module.exports = function(eleventyConfig) {
functiongetCommentChoices(data, ref) {
constmastodonUrl = data.mastodon || data.page?.mastodon || data.data?.mastodon;
constmastodon = mastodonUrl && `<a href="${mastodonUrl}" target="_blank" rel="noopener noreferrer">comment on Mastodon</a>`;
consturl = ref.absoluteUrl(data.url || data.permalink || data.data?.url || data.data?.permalink, data.metadata?.url || data.data?.metadata?.url);
constsubject = encodeURIComponent('Comment on ' + url);
constbody = encodeURIComponent("Name you want to be credited by (if any): \nMessage: \nCan I share your comment so other people can learn from it? Yes/No\n");
constemail = `<a href="mailto:sacha@sachachua.com?subject=${subject}&body=${body}">e-mail me at sacha@sachachua.com</a>`;
constdisqusLink = url + '#comment';
constdisqusForm = data.metadata?.disqusShortname && `<div id="disqus_thread"></div><script> var disqus_config = function () { this.page.url = "${url}"; this.page.identifier = "${data.id || ''} ${data.metadata?.url || ''}?p=${ data.id || data.permalink || this.page?.url}"; this.page.disqusTitle = "${ data.title }" this.page.postId = "${ data.id || data.permalink || this.page?.url }" }; (function() { // DON'T EDIT BELOW THIS LINE var d = document, s = d.createElement('script'); s.src = 'https://${ data.metadata?.disqusShortname }.disqus.com/embed.js'; s.setAttribute('data-timestamp', +new Date()); (d.head || d.body).appendChild(s); })();</script><noscript>Disqus requires Javascript, but you can still e-mail me if you want!</noscript>`;
return { mastodon, disqusLink, disqusForm, email };
}
eleventyConfig.addShortcode('comments', function(data, linksOnly=false) {
const { mastodon, disqusForm, disqusLink, email } = getCommentChoices(data, this);
if (linksOnly) {
return`You can ${mastodon ? mastodon + ', ' : ''}<a href="${disqusLink}">comment with Disqus (JS required)</a>${mastodon ? ',' : ''} or ${email}.`;
} else {
return`<div id="comment"></div>You can ${mastodon ? mastodon + ', ' : ''}comment with Disqus (JS required)${mastodon ? ', ' : ''} or you can ${email}.${disqusForm || ''}`;}
});
}
The new workflow I'm trying out seems to be working:
Keep npx eleventy --serve running in the
background, using .eleventyignore to make
rebuilds reasonably fast.
Export the subtree with C-c e s 1 1, which uses org-export-dispatch to call my-org-11ty-export with the subtree.
After about 10 seconds, use my-org-11ty-copy-just-this-post and verify.
Use my-mastodon-11ty-toot-post to compose a toot. Edit the toot and post it.
Check that the EXPORT_MASTODON property has been set.
Export the subtree again, this time with the front matter.
Publish my whole blog.
Next, I'm thinking of modifying
my-mastodon-11ty-toot-post so that it includes a
list of links to blog posts I might be building on
or responding to, and possibly the handles of
people related to those blog posts or topics.
Hmm…
Sometimes I want to copy a toot and include it in
my Org Mode notes, like when I post a thought and
then want to flesh it out into a blog post. This
code defines my-mastodon-org-copy-toot-content,
which converts the toot text to Org Mode format
using Pandoc and puts it in the kill ring so I can
yank it somewhere else.
(defunmy-mastodon-toot-at-url (&optional url)
"Return JSON toot object at URL.If URL is nil, return JSON toot object at point."
(if url
(let* ((search (format "%s/api/v2/search" mastodon-instance-url))
(params `(("q" . ,url)
("resolve" . "t"))) ; webfinger
(response (mastodon-http--get-json search params :silent)))
(car (alist-get 'statuses response)))
(mastodon-toot--base-toot-or-item-json)))
(defunmy-mastodon-org-copy-toot-content (&optional url)
"Copy the current toot's content as Org Mode.Use pandoc to convert.When called with \\[universal-argument], prompt for a URL."
(interactive (list
(when current-prefix-arg
(read-string "URL: "))))
(let ((toot (my-mastodon-toot-at-url url)))
(with-temp-buffer
(insert (alist-get 'content toot))
(call-process-region nil nil "pandoc" t t nil "-f""html""-t""org")
(kill-new
(concat
(org-link-make-string
(alist-get 'url toot)
(concat "@" (alist-get 'acct (alist-get 'account toot))))
":\n\n#+begin_quote\n"
(string-trim (buffer-string)) "\n#+end_quote\n"))
(message "Copied."))))
I usually summarize Mastodon links, move them to
my Emacs News Org file, and then categorize them.
Today I accidentically categorized the links while
they were still in my Mastodon buffer, so I had
two lists with categories. I wanted to write some
Emacs Lisp to merge sublists based on the
top-level items. I could sort the list
alphabetically with C-c ^ (org-sort) and then
delete the redundant top-level item lines, but
it's fun to tinker with Emacs Lisp.
Example input:
Topic A:
Item 1
Item 2
Item 2.1
Topic B:
Item 3
Topic A:
Item 4
Item 4.1
Example output:
Topic B:
Item 3
Topic A:
Item 1
Item 2
Item 2.1
Item 4
Item 4.1
The sorting doesn't particularly matter to me, but I want the things under Topic A to be combined. Someday it might be nice to recursively merge other entries (ex: if there's another "Topic A: - Item 2" subitem like "Item 2.2"), but I don't need that yet.
Anyway, we can parse the list with
org-list-to-lisp (which can even delete the
original list) and recreate it with
org-list-to-org, so then it's a matter of
transforming the data structure.
(defunmy-org-merge-list-entries-at-point ()
"Merge entries in a nested Org Mode list at point that have the same top-level item text."
(interactive)
(save-excursion
(let* ((list-indentation (save-excursion
(goto-char (caar (org-list-struct)))
(current-indentation)))
(list-struct (org-list-to-lisp t))
(merged-list (my-org-merge-list-entries list-struct)))
(insert (org-ascii--indent-string (org-list-to-org merged-list) list-indentation)
"\n"))))
(defunmy-org-merge-list-entries (list-struct)
"Merge an Org list based on its top-level headings"
(cons (car list-struct)
(mapcar
(lambda (g)
(list
(car g)
(let ((list-type (car (car (cdr (car (cdr g))))))
(entries (seq-mapcat #'cdar (mapcar #'cdr (cdr g)))))
(apply #'append (list list-type) entries nil))))
(seq-group-by #'car (cdr list-struct)))))
Because org-list-to-org uses the Org conversion
process, I need to make sure that my custom link
functions also export to Org as a format. For
example, in Emacs News, I use a package: link to
make it easy to link to packages in both Emacs and
in exported HTML. When I first ran my code, the
links got replaced with their URLs, which isn't
what I wanted. Turned out that I needed to add a
case handling exporting to org format, like
this:
Pedro pointed out that I had some incomplete clock
entries in my Emacs configuration.
org-resolve-clocks prompts you for what to do
with each open clock entry in your Org agenda
files and whatever Org Mode files you have open.
If you don't feel like cancelling each clock with
C, I also wrote this function to delete all open
clocks in the current file.