9 PM on a schoolday, time for me to nudge A+ off
to bed. A+ is clicking through the Stardew Valley
wiki, slurping up all sorts of trivia that she'll
probably trickle into our conversations. There are
two pieces of homework left to do, one with quite
a few slides to complete. And drawing. She's not a
big fan of drawing assignments. "My hand is
tired," she says.
I try to be calm and supportive. I wobble.
Could've done this earlier, I think. I manage to
keep myself from saying it. I teeter, noticing
myself mentally fast-forwarding decades ahead. Oh
no, she's not going to get the hang of doing
things that she finds boring, she won't develop
study skills or executive control, she'll cram
through all the classes she can coast in, and all
of it will come crashing down in university when
she might actually need to buckle down and study.
She's 9! She's a long way from university.
I'm learning to embrace my anxiety and appreciate
how it tries to keep us all safe. This feeling
makes sense. I want to help her avoid mistakes,
especially when the feedback cycle is long and the
results of choices will only be seen much later.
But anxiety gets in the way of parenting. If I let
the fearful part of my brain take over, I'll
inadvertently teach her that mistakes are
catastrophic rather than just ordinary Tuesdays. I
want to hold her steady, but the wobbles are how
we learn.
It's somewhat manageable now, when we can talk
about these things openly. A+ can laugh off my
worries ("Mom, you're fretting again,") and W- can
remind me to slow down when it runs away with me.
He's usually pretty chill about all this. It'll be
harder when the cognitive rewiring of puberty or
menopause turn ordinary conversations into
minefields right when the stakes get higher. The
more I tighten my grip, the more star systems will
slip through my fingers. (There I go again with
catastrophizing.)
Besides, I want to help A+ avoid the paralysis of
perfectionism or self-recrimination. I want her to
be able to experiment, and to pick herself up and try
again if things don't work out the first time
around. To do that, I need to learn to change my
perspective from being anxious about mistakes to
seeing the opportunities for re-takes.
There are many things I can't teach A+. Some
things can't fully be taught, they can only be
learned, like how to balance the clay on the
pottery wheel. Sometimes I don't even know what
the right answer would be, like what kinds of tips
work for her particular brain. Some things change
over time and she'll need to change with them,
like how to adapt to life's situations. She'll
need to learn how to learn instead of relying on
one fixed answer.
Fortunately, life comes with so many opportunities
to practise. The Toronto public school calendar
has 187 instructional days, so she gets plenty of
chances to manage her homework and get feedback.
The repetitive nature of things used to frustrate
me when it came to my tasks (always more dishes to
wash, always more clothes to fold), but it's good
for learning, especially while the stakes are low.
It's her experiment, I remind myself. About half
the time, she doesn't even want my help. ("I can
do it, Mom.") She's sensible enough to try
things out on small experiments instead of scary
ones: shopping at the grocery store on her own,
not skydiving.
There's plenty of stuff for me to learn while she
learns. When I get the urge to correct her work
("How does that line up with the rubric?") or nag
her to get her work done, I tell myself:
Is it really a problem? The teacher isn't
expecting her to completely master all the
skills, and the teacher is in a good place to
give developmentally-appropriate feedback. I can
let her experiment with how much work she wants
to put into things, and she can see what that
results in. Despite all my twitchiness about how
she puts off her daily homework until 9 PM, she
still manages to get things done. Judging from
the frequent reminders her teacher gives the
virtual class, she's probably ahead of the
curve. So maybe it's not a problem.
Whose problem is it? Something might not be my
problem. It might not even be her problem. She
reads during class time, for instance. Sometimes
she misses something that can't be figured out
from just the homework slide deck. Maybe that's
partly her experimenting to find the right
balance between attention and stimulation. Maybe
that's also partly a consequence of how school
is designed to go at the group's pace. Not
entirely her problem.
The more I let go of the small stuff, the more
experience she'll be able to draw on for the big
stuff. I hope she'll get the hang of thinking of
life as mostly series of little experiments, and
to notice when there's a bigger choice that needs
more thinking because it's more long-term. The
more she decides, the more confidence we both
develop in her decisions.
This reminds me of how kids learn how to bike. The
popular approach uses training wheels to prevent
falls. The idea is to gradually raise them as the
kid improves, but I usually see kids pedaling
along (perhaps slightly leaning over to one side)
to match the slowness of parents' walking. It's
hard to balance when you're going slow. But
pedaling isn't the hard part. Balancing is, and
you develop balance by balancing. Maybe that's a
little like how I get tempted to rescue A+ from
the results of some of her choices, but letting
her try things is how to help her learn.
A+ learned how to bike using a balance bike
instead of using training wheels. When she was
two, she toddled along on a Strider, which was
light enough for her to manage. Eventually she
figured out coasting. She was proud of being able
to do it on her own. Then we upgraded her to a
Cleary Gecko freewheel bike, with proper
hand-brakes and everything. After a few attempts
with us holding her under her armpits, she was
ready for us to steady her with a hand on her
back, and then for us to be close, and then she
was off on her own. She fell and skinned her knee
many times, developing an appreciation of pants
for protection and ice cream for comfort. The more
she biked, the more she learned how to notice that
feeling of being slightly off-balance, and the
better she got at correcting it. Now we can bike
on the streets together.
You can't learn how to bike if the training wheels
are always on, or if someone's always holding you
steady. It's okay to wobble and fall and get up.
You learn that you can survive a skinned knee, and
so you keep going.
Sometimes, when A+'s in the middle of a meltdown,
I have to remind myself not to try to fix it in
the moment. That doesn't work, anyway. Just take
the loss and try again next time. Sometimes, once
we've both calmed down, I ask A+ to imagine
rewinding back to a situation so we can play it
out a little differently. Sorry, I meant to say
this, not that. Would that work better? Next time.
Not mistakes. Data. Just another step in the
journey.
Getting better at getting better helps me, too.
I've been practising piano, making steady progress
through the Simply Piano app. I've been playing
for about four months now. I took piano lessons as
a kid, but not to any serious extent. Back then, I
got bored with the simple exercises I had to do.
Now I feel slow, snail-slow, but I can savour the
way my mind is beginning to get the hang of
things, knowing that it will take me many tries to
get the hang of it. I'm starting to be able to
look at the notes and remember the phrases,
imagine what the next sequence will sound like
before I play it, and notice how my hands move to
make it happen.
When my fingers wobble on the keys, I slow down
and try again. There's no point in berating
myself. If my mind keeps hiccuping or my fingers
keep stumbling, I can think: ah, is this because
I'm tired, or because I want to do something else,
or just because I'm learning and it takes time to
get the hang of things? I'm getting better at
figuring out when I should probably call it a day
so that I don't practise mistakes into my muscle
memory and when I might benefit from just slowing
down the segment.
I still stumble through pieces I've successfully
played before. Remembering is hard. But I'm
getting better at being patient with myself,
accepting that it's because I'm still in the
middle of the journey. It's not a mistake that I
should grump at myself about. It's just part of a
re-take. This is what learning looks (and sounds)
like. Of course it doesn't start out perfectly
smooth.
Here's me learning Mozart's "Rondo alla Turca",
with the app providing accompaniment in the
background. It's not perfect, but it's progress.
・・・・・
We were at the playground. I ate the remaining
crackers in the snack box because I thought A+ was
done. Turns out she was saving them for later. She
was very upset. I apologized and promised to ask
next time, but she was too far gone to hear.
That was a tough moment. A+ was already
emotionally off-balance because the playdate
hadn't gone as well as it usually does.
Discovering I had eaten the crackers she was
looking forward to was the last straw. She
dissolved into tears. I snuggled her and settled
in for a long wait. I think: Where's the line
between comforting her and coddling her? Does my
anxiety teach her this is too hard to handle?
We're not quite at the point of being able to
shrug off mistakes. I remind myself that she'll
learn what she's ready to learn.
Looking around while A+ drenched my left shoulder,
I noticed a skateboarder on the park road. Maybe a
man in his thirties? He was trying to jump his
skateboard over a low concrete lane divider. He
had been at it for a while before I noticed. Most
times, he was able to clear the divider, but the
skateboard slowed down too much on the other side
and he had to jump off. On the seventh try that I
saw, he landed back on the skateboard and rolled
on for a bit. Success! He tried again and failed.
Four more failures before his next success. One
more attempt–another failure–and then he called
it a day. I'm sure he'll be back at it.
A+ continued to cry. My phone buzzed, reminding me
that we probably wanted to get going before the
rain in the forecast. I carried her as I picked up
our bags and put them in my bike. Eventually I
needed to gradually ease her off me. She curled up
in the bucket of my front-loader cargo bike, still
crying. I tucked the towel around her like a
blanket, buckled her bike into the tow-bag, and
walked the bikes home. She fell asleep.
A wobble, a fall. But I'm sure we'll be back at it
too. (And we did; the next day, she was happily
playing with her friends again.)
・・・・・
It's hard to be in the moment. Sometimes the
moment sucks. It's hard to be far ahead in the
future. It makes decisions feel too big. Do-overs
make things just the right size. If we can get
good at shrugging off the inevitable failures and
treating them as data so that we can sketch out
ideas for the next experiment, I think that'd be
pretty cool. Instead of "Oh no!" or even "Are you
sure about that?" (what kid likes to be
doubted?), I can lean towards, "Hmm, let's find
out."
As predicted, we had another late-night homework
situation. This time she had a headache and wanted
to go to bed, homework unfinished. I was able to
let go and just focus on snuggling her in. The
next day, after morning routines and without any
nagging, she did the homework and submitted it.
Late, but done.
There'll be another bedtime homework session,
I'm sure. I have to trust that even though I want
to shortcut the learning for her, she's got this.
She's figuring things out. If we stumble, that
just helps us practice for next time, and there
are so many opportunities to try again. The wobble
is not the obstacle, it's the way.1
Related: The Obstacle is the Way, Ryan Holiday's book on Stoicism; the title rephrases this thought from Marcus Aurelius's Meditations: "… and that which is an obstacle on the road
helps us on this road."
Sometimes I just need to rewind 15 minutes.
That's the length of A+'s recess at virtual
school, which she does at home. At recess, she
often likes to get hugs and sometimes a snack. If
I'm working on something that requires sustained
thought, like code or a blog post, I can't hold
those thoughts in my head for that long while
cheerfully listening to A+ share the trivia she's
read on the Stardew Valley wiki. If I try to keep
my train of thought, I get grumpy. I'd rather get
better at time travel instead. Naturally, this
calls for Emacs.
For people who are unfamiliar with Emacs or Org Mode
GNU Emacs is a highly customizable program for
editing text, writing code, and doing mostly
whatever people want to get it to do. Org Mode is
a package (or ecosystem of packages, really) that
modifies GNU Emacs to make it easier to take
notes, plan tasks, export documents, and so on. If
you're not into Emacs yet, this post might be a
little technical, but maybe there are ways to
translate some of the ideas to things you're
using.
Sometimes recess totally resets my brain and I
can't even think of what I was just working on. To
make it easier to hit the ground running, I try to
make a habit of creating a task in Org Mode before
I start working on it. Or, more realistically,
halfway through, when I realize I have to first do
another thing, so then I jot down a quick task
for the work I was previously doing and another
task for the tangent I'm about to go on. That way,
I can quickly check my notes to see what I was
doing. org-capture (which I've bound to C-c r)
is handy for that. I have a template (t) that
creates a timestamped TODO that links to the
context I created it in (files, note headings,
etc.) and saves it to my inbox file. Then I can
jump to my inbox file with a keyboard shortcut and
look at what I need to get back to doing.
Sometimes I vaguely remember that I've already
created a task for this before and I can find it
with C-u C-c C-w (org-refile). When
org-refile is called with a universal prefix
argument (C-u), it will prompt for a heading in
org-refile-targets and jump to it. I have it set
to complete the outline path, so I can try to find
things by project. Failing that, I might have a
quick rummage in my inbox. I usually don't
remember the exact words I used in the the task
title, though. Maybe someday I'll get the hang of
org-ql or p-search (Emacsconf talk on p-search),
resurrect the Remembrance Agent so that it can
continuously do bag-of-words matching, or use
embeddings to find semantically similar tasks and
notes. In the meantime, capturing the task is more
important than avoiding duplicates. I can find and
clean up duplicates later on.
All of that is moot when I'm away from my
computer, which is most of the time. My phone is
pretty handy for quick notes, though. I use Orgzly
Revived to capture a quick note in my inbox. This
gets synchronized with my Org Mode notes using
Syncthing.
Hmm, I gotta do this first…
Often the interruption doesn't even come from
outside, but from my brain's endless stream of
interesting ideas. Some of those ideas can be
saved as tasks to work on eventually, but
sometimes I need to pause my current task and work
on the new idea. I have a template for an
interrupting task (i) that automatically clocks
out of the previous task and clocks into the new
one.
("i""Interrupting task" entry
(file ,my-org-inbox-file)
"* STARTED %^{Task}\n:PROPERTIES:\n:CREATED: %U\n:END:\n%a\n":clock-in:clock-resume:prepend t)
Okay, that's done, what was I doing before?
If I clock into tasks, I can use org-clock-goto
along with the C-u universal prefix (C-u C-c
C-x C-j) to see a list of recently-clocked-in
tasks. This is great for "popping the stack,"
which is how I think of backtracking once I
finished an interrupting task.
I usually forget to clock out. That's okay. I'm
not looking for precise total times, just
breadcrumbs.
… What was I thinking?
Sometimes a few keywords aren't enough to jog my
memory. Whenever I think, "Ah, this is easy, I
don't need to take notes," I inevitably regret it.
Sometimes I realize I have to re-do my thinking
fifteen minutes later, when singing 4-Town songs
with A+ has pushed those thoughts out of my brain.
Sometimes I have to re-do my thinking several
months later, which is even harder.
Notes are super-helpful. I love the way Org Mode
lets me write notes, paste in hyperlinks, add
snippets of code, save the results of my
explorations, include my sketches, and even export
them as blog posts or documents to share.
Sometimes I have to go back farther into the past
It can take me months or even years before
I can circle back to a project or idea. It can be
hard to reconstruct my thinking after a lot of
time has passed, so it's good to write down as
much as possible. Taking notes feels slower than
just plunging ahead, but they help me travel back
in time to try to remember.
This really gets hammered in when I run into
things I've forgotten, like when I dusted off my
time-tracking code so I could make some changes.
In the four years that elapsed between Aug 2020
(my last change) and Oct 2024 (when I decided to
upgrade it to the latest version of Rails), I'd
forgotten how to even run a development version of
my code. Whoops. I ended up taking more notes
along the way.
I try to keep project-related notes as close to
the project files as possible, like a README.org
in the project directory. Sometimes I don't even
remember what the project is called. I try to keep
another file that indexes things on my computer as
well as things in real life.
Sometimes I know I wrote tasks or notes down
before but I can't remember the exact words I used
for them. I'm curious about whether embeddings
might help me find those things again. So far it's
been okay to just add a new task or note, and then
periodically clean up entries that are no longer
needed.
Going sideways
Sometimes I want to visit
alternate timelines, trying different ways to do
something. I really like the way undo works in
Emacs. It's different from most programs. Emacs
keeps the things you undo/redo.
Let's say I start writing a paragraph or a piece
of code. I change my mind about something. Maybe I
undo, maybe I cut, maybe I delete. I write again.
I change my mind again. The first way was better,
maybe. I can go back to that, step through any of
the intermediate changes, consider the other
version again. It's not lost.
Actually navigating the Emacs undo history can be
tricky. I like using the vundo package for that.
It shows a compact view of the different branches
of this timeline so that I can easily jump between
them or compare them.
If I'm working on something more complicated, like
code, I might make changes over several sessions.
This is where version control is handy. I like
using the Git version control system, especially
with the Magit package. I can commit versions of
the files manually along with a quick note about
what I changed or what I'm trying. This allows me
to easily reset to a certain point.
Sometimes I'm good about picking the right point
to commit: I've made decent progress and things
are working. Sometimes I realize only later on
that I probably should have saved a commit a
little while ago, and now I'm halfway through
another idea that I'm not going to have time to
finish and that leaves my project in a non-working
state. In that situation, sometimes I'll use the
visual undo provided by the vundo package to go
backwards to a version that looks about right,
save that file, commit it with a quick note, and then go forward in
time again.
Saving revisions in Git makes it much easier to go
backwards in time even if I've restarted my
computer. magit-blame and vc-annotate give me
slightly different views showing me the changes in
a file. They don't show me information on deleted
sections, though. For that, I can use the
magit-diff command to compare versions.
Sometimes it's easier to flip through the history
of a single file with git-timemachine.
Git lets me name different experimental timelines
(branches) and logically group changes together.
It means I don't have to worry so much about
messing up a working file, since I can travel back
in time to that version. It also means I can
easily compare them to see what I've changed so
far.
In addition to using version control for various
projects, I also save backup files to a separate
directory by setting my backup-directory-alist
to (("." . "~/.config/emacs/backups")). Disk
space is cheap; might as well keep all the
backups. I sometimes manually go into this
directory to find older versions of things. It
occurs to me that it might be good to flip through
the backups in the same way that git-time-machine
makes it easy to flip through git revisions. I'm
trying out lewang/backup-walker, which shows the
incremental diffs between versions. It was last
updated 12 years ago(!), but can easily be dusted
off to work with Emacs 30 by defining some
functions that it's looking for. Here's my config
snippet:
It's not all about going back to the past.
Sometimes I want to plan ahead: tasks that I want
to schedule for a certain date, pre-mortems to
help me make decisions, gifts for my future self.
I use Google Calendar for appointments and other
things I might want to share with W- for planning,
but there are lots of other things that aren't
tied to a specific time and date. The agenda
feature of Org Mode is handy for scheduling things
and moving them around.
Scheduled tasks don't work out so well if my
agenda gets cluttered by things I ignore, so if I
find myself procrastinating something a lot, I
think about whether I really want to do whatever
it is I've written down.
Some notes aren't associated with specific dates,
but with other events that might happen. I have an
Org Mode outline with various subheadings under
"In case of…", although I often forget to check
these or have a hard time finding them again.
Maybe someday I can write a script that analyzes
the words I use in my journal entries or tasks and
finds the notes that approximately match those
keywords.
Things I want to try
Thinking out loud more might be worth
experimenting with, since I can do that while I'm
working in a different file. I've used my audio
recorder to record braindumps and I have a
workflow for transcribing those with OpenAI
Whisper. I think it would be even more useful to
have an org-capture equivalent so that I can
capture the thought by audio, save the recording
in case there are recognition errors (highly
likely because of the technical terms), and save
the context. Or maybe an even neater interface
that keeps an ear out for keywords, executes
commands based on them, and saves the rest as
notes? whisper-ctranslate2 has a live_transcribe
option that works reasonably well after a short
delay, and maybe I can use a process filter to
pull the information out or write a custom Python
script.
I appreciate how working with plain text can help
me jump backward or forward in time. I'm looking
forward to seeing how this can be even better!
I've recently started handling the Bike Brigade
newsletter, so now I'm itching to solve the little
bits of friction that get in my way when I work
with the rich-text Mailchimp block editor.
I'm not quite ready to generate everything with
Org Mode. Sometimes other people go in and edit
the newsletter through the web interface, so I
shouldn't just dump a bunch of HTML in. (We don't have the more expensive plan that would allow me to make editable templates.) I draft the newsletter as a Slack canvas so more people can weigh in with their suggestions:
Figure 1: Screenshot of Slack canvas
And then I redo it in Mailchimp:
Figure 2: Screenshot of Mailchimp design
My process is roughly:
Duplicate blocks.
Copy the text for each item and paste it in. Adjust formatting.
Update the dates and links. Flip back and forth between the dispatch webpage and Mailchimp, getting the links and the dates just right.
Download images one by one.
Replace the images by uploading the saved images. Hunt through lots of files named image (3).png, image (4).png, and so on. Update their attributes and links.
Change text and link colours as needed by manually selecting the text, clicking on the colour button in the toolbar, and selecting the correct colour.
Change the text on each button. Switch to Slack, copy the link, switch back to Mailchimp, and update the link.
The newsletter includes a button to make it easier
to volunteer for deliveries. In case people want
to plan ahead, I also include a link to the
following week's signups.
Dates are fiddly and error-prone, so I want to
automate them. I can use a Mailchimp code block to
paste in some HTML directly, since I don't think
other people will need to edit this button. Here I
take advantage of org-read-date's clever date
parsing so that I can specify dates like +2Sun
to mean two Sundays from now. That way, I don't
have to do any date calculations myself.
This code generates something like this:
Figure 3: Screenshot of buttons
Text from the screenshot
SIGN UP NOW TO DELIVER JUN 23-29
You can also sign up early to deliver Jun 30-Jul 6
Here's the code. It calculates the dates, formats
the HTML code. I use format-time-string to
format just the month part of the dates and
compare them to tell if I can skip the month part
of the end date. After the HTML is formatted, the
code uses xdotool (a Linux command-line tool) to
switch to Google Chrome so that I can paste it in.
Now I can use an Org Mode link like
elisp:my-brigade-copy-signup-block to generate
the HTML code that I can paste into a Mailchimp
code block. The button link is underlined even
though the inline style says
text-decoration:none, but it's easy enough to
remove that with Ctrl+u.
Transforming HTML
The rest of the newsletter is less
straightforward. I copy parts of the newsletter
draft from the canvas in Slack to the block editor
in Mailchimp. When I paste it in, I need to do a
lot to format the results neatly.
I think I'll want to use this technique of
transforming HTML data on the clipboard again in
the future, so let's start with a general way to
do it. This uses the xclip tool for command-line
copying and pasting in X11 environments. It parses
the HTML into a document object model (DOM), runs
it through various functions sequentially, and
copies the transformed results. Using DOMs instead
of regular expressions means that it's easier to
handle nested elements.
(defvarmy-transform-html-clipboard-functions nil "List of functions to call with the clipboard contents.Each function should take a DOM node and return the resulting DOM node.")
(defunmy-transform-html-clipboard (&optional activate-app-afterwards functions text)
"Parse clipboard contents and transform it.This calls FUNCTIONS, defaulting to `my-transform-html-clipboard-functions'.If ACTIVATE-APP-AFTERWARDS is non-nil, use xdotool to try to activate that app's window."
(with-temp-buffer
(let ((text (or text (shell-command-to-string "unbuffer -p xclip -o -selection clipboard -t text/html 2>& /dev/null"))))
(if (string= text "")
(error"Clipboard does not contain HTML.")
(insert (concat "<div>"
text
"</div>"))))
(let ((dom (libxml-parse-html-region (point-min) (point-max))))
(erase-buffer)
(dom-print (seq-reduce
(lambda (prev val)
(funcall val prev))
(or functions my-transform-html-clipboard-functions)
dom)))
(shell-command-on-region
(point-min) (point-max)
"xclip -i -selection clipboard -t text/html -filter 2>& /dev/null"))
(when activate-app-afterwards
(call-process "xdotool" nil nil nil "search""--onlyvisible""--all" activate-app-afterwards "windowactivate""windowfocus")))
Saving images
Images from Slack don't transfer cleanly to
Mailchimp. I can download images from Slack one at
a time, but Slack saves them with generic
filenames like image (2).png. Each main
newsletter item has one image, so I'd like to
automatically save the image using the item title.
When I copy HTML from the Slack canvas, images are
included as data URIs. The markup looks like this:
<img src='data:image/png;base64,iVBORw0KGgo...
With the way I do the draft in Slack, images are
always followed by the item title as an h2
heading. If there isn't a heading, the image just
doesn't get saved. If there's no image in a
section, the code clears the variable, so that's
fine too. I can parse and save the images like
this:
For easier testing, I used xclip -o -selection
clipboard -t text/html > ~/Downloads/test.html to
save the clipboard. To run the code with the saved
clipboard, I can call it like this:
Mailchimp recommends using buttons for calls to
action so that they're larger and easier to click
than links. In my Slack canvas draft, I use [ link
text ] to indicate those calls to action. Wouldn't
it be nice if my code automatically transformed
those into centered buttons?
I also want to change the link colours to match
the colour scheme. The newsletter has two parts
distinguished by background colours. Bike Brigade
updates use black text on a white background, and
community updates use white text on a dark blue
background so that they're visually distinct. For
contrast, I like to use light blue links in the
community section, which doesn't match the colour
of the links when I paste them in from Slack. This
meant manually recolouring the text and each of
the links in Mailchimp, which was tedious.
Figure 5: Screenshot of community update colours
This code changes the colours of the links. It
also changes the colours of text by wrapping spans
around them. It skips the links we turned into
buttons.
(defvarmy-brigade-community-text-style"color: #ffffff")
(defvarmy-brigade-community-link-style"color: #aed9ef")
(defunmy-brigade-recolor-recursively (node)
"Change the colors of links and text in NODE.Ignore links with the class mceButtonLink.Uses `my-brigade-community-text-style' and `my-brigade-community-link-style'."
(pcase (dom-tag node)
('table node) ; pass through, don't recurse further
('a; change the colour
(unless (string= (or (dom-attr node 'class) "") "mceButtonLink")
(dom-set-attribute node 'style my-brigade-community-link-style))
node)
(_
(let ((processed
(seq-map
(lambda (child)
(if (stringp child)
(dom-node 'span`((style . ,my-brigade-community-text-style)) child)
(my-brigade-recolor-recursively child)))
(dom-children node))))
`(,(dom-tag node) ,(dom-attributes node) ,@processed)))))
Now that I've made all those little pieces, I can put them together in two interactive functions. The first function will be for the regular colour scheme, and the second function will be for the light-on-dark colour scheme. For convenience, I'll have it activate Google Chrome afterwards so that I can paste the results into the right block.
Since this pastes the results as formatted text, it's
editable using the usual Mailchimp workflow. That
way, other people can make last-minute updates.
With embedded images, the saved HTML is about 8
MB. The code makes quick work of it. This saves
about 10-15 minutes per newsletter, so the time
investment probably won't directly pay off. But it
also reduces annoyance, which is even more
important than raw time savings. I enjoyed
figuring all this out. I think this technique of
transforming HTML in the clipboard will come in
handy. By writing the functions as small,
composable parts, I can change how I want to
transform the clipboard.
Next steps
It would be interesting to someday automate the campaign blocks while still making them mostly editable, as in the following examples:
(Also, hat tip to this Reddit post that helped me
get xclip to work more reliably from within Emacs
by adding -filter 2>& /dev/null to the end of my
xclip call so it didn't hang.)
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:
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.
This 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!