Doodling icons in a grid

| drawing, emacs

Last week, I experimented with practising drawing little icons as a way of expanding my visual vocabulary.

Making a template

Building on Visual vocabulary practice - ABCs, I decided to make a regular grid that I could then automatically split up into individual images. I used Emacs's svg.el to generate the grid. I started with 4 rows of 7 boxes to match the alphabet example, but I realized that using 5 rows of 7 boxes each would let me reuse the grid for a monthly calendar. I numbered the boxes to make it easier to double-check if the lists line up, but I can write over the numbers for things like dates since the background won't be exported.

icon-grid.png

I used convert icon-grid.svg icon-grid.png to make it from the SVG produced by the following code.

Code for producing the template

(require 'svg)
(defvar my-dot-grid-boxes-params
  '(:num-rows 5
    :num-cols 7
    :dot-size 3
    :line-width 3
    :dot-spacing 60
    :grid-color "#a6d2ff"
    :row-size 6
    :col-size 6
    :text-size 50
    :margin-top 2))
(cl-defun my-dot-grid-boxes-template (&key (num-rows 5)
                                           (num-cols 7)
                                           (dot-size 3)
                                           (line-width 3)
                                           (dot-spacing 60)
                                           (grid-color "#a6d2ff")
                                           (row-size 6)
                                           (col-size 6)
                                           (text-size 50)
                                           (margin-top 2))
  "Prepare an SVG with a dot grid within a table with solid gridlines.
Each dot is a solid circle of DOT-SIZE filled with GRID-COLOR spaced DOT-SPACING apart.
The gridlines are also GRID-COLOR. They should divide the image into ROWS and COLUMNS, which are ROW-SIZE * DOT-SPACING and COL-SIZE * DOT-SPACING apart.
The table has a top margin with the dot grid, and this is MARGIN-TOP * DOT-SPACING tall.
All dots are centered on their x, y coordinates.
The rest of the image's background is white."
  (let* ((width (* num-cols col-size dot-spacing))
         (height (* dot-spacing (+ margin-top (* num-rows row-size))))
         (margin-top-height (* margin-top dot-spacing))
         (svg (svg-create width height)))
    (dotimes (row (+ (* num-rows row-size) margin-top))
      (dotimes (col (1+ (* num-cols col-size)))
        (let ((x (* col dot-spacing))
              (y (* row dot-spacing)))
          (svg-circle svg x y dot-size
                      :fill-color grid-color
                      :stroke-width 0))))
    (when (> text-size 0)
      (dotimes (i (* num-rows num-cols))
        (let ((x (* (% i num-cols) col-size dot-spacing))
              (y (+ margin-top-height (* (/ i num-cols) row-size dot-spacing))))
          (svg-text svg
                    (number-to-string (1+ i))
                    :x x :y (+ y text-size)
                    :fill-color grid-color
                    :font-size text-size
                    :stroke-width 0))))
    (dotimes (col (1+ num-cols))
      (let ((x (* col col-size dot-spacing)))
        (svg-line svg x margin-top-height x height
                  :stroke-color grid-color
                  :stroke-width line-width)))
    (dotimes (row (1+ num-rows))
      (let ((y (+ margin-top-height (* row row-size dot-spacing))))
        (svg-line svg 0 y width y
                  :stroke-color grid-color
                  :stroke-width line-width)))
    svg))

With that function defined, I can make a template with:

(with-temp-file "~/Dropbox/sketches/icon-grid.svg"
  (svg-print
   (my-dot-grid-boxes-template)))

I used that template to draw a bunch of little doodles. The Noteful app I use on my iPad makes it easy to import a template and then export my drawings without including the template.

(If this blog post is out of date, you can check the Dot-grid box templates section in my config for my current code.)

I've done this sort of thing before, when I made a font based on my handwriting. That time, I used Python to generate the template with sample characters, and I used Python again to cut the exported image into individual glyphs.

The drawings

Once I imported the template into Noteful, it was easy to draw using fragments of time. 35 boxes are a lot, but each icon was just a few minutes of drawing, and I enjoyed seeing the progress.

Stream of consciousness

Sometimes I drew whatever came to mind:

Text from sketch

Drawing practice 2025-09-24-09

me A+ pizza mom and kid flower witch hat pencil chopsticks rice bowl peach pillow desk fan folding fan pumpkin jack o' lantern ghost taxes broomstick bubbles candy bow bao bowl strawberry tomato cherries cake slice cake

Text from sketch

Drawing practice 2025-09-25-01

mug teacup tempest in a teapot skull poison cauldron tree baseball cap propeller beanie top hat magic magic wand cape playing card hanging towel folded towels soap dispenser bar soap picnic table picnic basket bread croissant donut donut sandwich soup bowl rice and eggs oatmeal

Text from sketch

Drawing practice 2025-09-26-01

clogs slippers slipper tic-tac-toe stockpot skillet crepe pan crepe pancakes cereal sun sailboat crown see saw ice cream cupcake with icing dress and pants rice cooker leap heart heart - anatomical eye headband hairpin bandage glasses glass straw air purifier mask - KN95 pie pie slice pie chart orange lemon

Text from sketch

Drawing practice 2025-09-26-02

trash can garbage can chef's knife paring knife steak knife bread knife butter knife egg egg shells scrambled egg toast bean peas peas hot dog in a bun hot dog octopus avocado taco milk yogurt applesauce chicken drumstick sushi - hand roll lamp present presentation audience applause almond bitter melon oil chopping board partly cloudy rainy cloudy

Learning from books

Other times, I tried systematically going through the doodling books I checked out from the library:

Text from sketch

From "How to Draw Almost Every Day" - Kamo 2025-09-26-05

sake bottle sake cup brush snowflake kite top cat sleeping orange rice cake ornament notebook kimono shopping bag pencil eraser thermometer medicine scarf mittens glove hat boot coat skate snowman shovel washer refrigerator microwave laundry convenience store blimp spatula hot pot bonsai coffee maker

Text from sketch

From "How to Draw Cute Doodles and Illustrations" - Kamo 2025-09-29-05

enjoyment crying happy or asleep making a mistake sleepy yum or cheeky cheerful or excited smiling confusion anger unsettled discomfort front view rear view side view sitting on a chair teacher baby kids (1-3) kids (4-5) walking running jumping raising a hand sitting on the floor swinging singing drawing sunny rain cloudy windy stormy snow moon and stars

Text from sketch

From "How to Draw Cute Doodles and Illustrations" – Kamo 2025-09-30-05

cat cat profile dog dog cat napping cat sitting upright dog dog (fluffy) rabbit monkey mouse cheetah bear raccoon dog fox squirrel lion koala pig elephant sheep giraffe horse bird (front) bird (profile) duck owl swan sparrow nest peacock chicken, chick, egg stork fish whale

Extracting icons from my other sketches

I also extracted the stick figures and cats I'd drawn for different emotions.

Text from sketch

Stick figure feelings 2025-09-30-05

playful content interested proud accepted powerful peaceful trusting optimistic startled confused tired busy amazed stressed stressed excited bored scared mad aggressive frustrated frustrated let down bitter weak weak anxious distant critical humiliated rejected threatened insecure insecure

Text from sketch

Stick figure and feline feelings 2025-09-30-06

lonely vulnerable despair guilty depressed hurt awful disapproving repelled disappointed startled confused bored scared excited tired let down rejected insecure anxious threatened humiliated cheeky interested peaceful successful content aggressive accepted trusting proud vulnerable optimistic lonely frustrated

Splitting up the drawings into individual components

Because I kept all my doodles within the template's boxes, it was easy to split up the images into individual files. First, I needed the text for all the labels. Sometimes I typed this in manually, and sometimes I used Google Cloud Vision to extract the text (editing it a little bit to put it in the right order and fix misrecognized text). Then I used Emacs Lisp to read the labels from the text file, calculate the coordinates, and use ImageMagick to extract that portion of the image into a file. I used filenames based on the label of the individual icon and the ID of the image it came from.

Code for extracting the icons

(cl-defun my-dot-grid-boxes-list (&key (num-rows 5)
                                       (num-cols 7)
                                       (dot-spacing 60)
                                       (row-size 6)
                                       (col-size 6)
                                       (text-bottom 1)
                                       (margin-top 2)
                                       filename
                                       &allow-other-keys)
  "Return a list of boxes."
  (let* ((margin-top-height (* margin-top dot-spacing))
         (max-image-size nil)
         (size (image-size (create-image filename nil nil :scale 1) t))
         (ratio (/ (car size) (* num-cols col-size dot-spacing 1.0)))
         results)
    (message "Expected adjusted height %f actual height %f"
             (* (+ margin-top (* num-rows row-size)) dot-spacing ratio)
             (cdr size))
    (dotimes (i (* num-rows num-cols))
      (let* ((r (/ i num-cols))
             (c (% i num-cols))
             (y (* (+ margin-top-height (* r col-size dot-spacing)) ratio))
             (x (* c row-size dot-spacing ratio))
             (width (* col-size dot-spacing ratio))
             (height (* (- row-size text-bottom) dot-spacing ratio)))
        (setq results (cons
                       `((r . ,r)
                         (c . ,c)
                         (i . ,i)
                         (x . ,(floor x))
                         (y . ,(floor y))
                         (w . ,(floor width))
                         (h . ,(floor height))
                         (x2 . ,(floor (+ x width)))
                         (y2 . ,(floor (+ y height))))
                       results))))
    (nreverse results)))

(defvar my-sketch-icon-directory "~/sync/sketches/icons")
(cl-defun my-dot-grid-boxes-extract (&rest args &key filename labels
                                           (output-dir my-sketch-icon-directory) force &allow-other-keys)
  (let* ((list (apply #'my-dot-grid-boxes-list args))
         (base (file-name-base filename))
         (ext (concat "." (file-name-extension filename)))
         (id
          (if (string-match "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]-[0-9][0-9]" base)
              (match-string 0 base)
            ""))
         results
         args)
    (dolist (icon list)
      (let-alist icon
        (let ((new-filename (expand-file-name
                             (concat (my-make-slug (elt labels .i)) "--"
                                     id
                                     (format "-%d-%d"
                                             .r .c)
                                     ext)
                             output-dir)))
          (push `((term . ,(elt labels .i))
                  (icon . ,(file-name-nondirectory new-filename))
                  (source . ,(file-name-nondirectory filename)))
                results)
          (when (or force (not (file-exists-p new-filename)))
            (setq args
                  (list (expand-file-name filename)
                        "-crop"
                        (format "%dx%d+%d+%d" .w .h .x .y)
                        "+repage"
                        new-filename))
            (message "%s" (concat "convert " (mapconcat #'shell-quote-argument args " ")))
            (apply #'call-process "convert" nil nil nil args)))))
    (nreverse results)))

(defun my-dot-grid-boxes-labels (id)
  (with-temp-buffer
    (insert-file-contents (concat (file-name-sans-extension (my-get-sketch-filename id)) ".txt"))
    (goto-char (point-min))
    (re-search-forward "^ *$")
    (split-string (string-trim (buffer-substring (point) (point-max))) "\n")))

2025-09-30_13-13-52.png
Figure 1: Dired and image-dired in Emacs

I really liked being able to write code to extract and name images all in one go. If you don't want to dive into Emacs Lisp, though, you can slice up a large image into small ones using ImageMagick.

Thinking ahead: if I use a similar process for my daily drawings, I can extract an "on this day" slice like the one I have for blog posts and sketches (blog post about it).

I had worked on a similar visual vocabulary project in 2013, but I had made it as a shared notebook in Evernote. That's gone now, and I can't remember if I backed it up or where I would've saved a backup to. Ah well, no harm in starting again, with files under my control.

Looking up images

Now that I'd broken down the images into labelled components, I wanted to be able to quickly look up icons from a web browser; my own version of The Noun Project. First, I exported the label information into a JSON.

Code for processing a sketch and updating the index

(defun my-sketch-icon-update-index (list)
  (let (data
        (index-file (expand-file-name "index.json" my-sketch-icon-directory)))
    (with-temp-file index-file
      (setq data
            (if (file-exists-p index-file)
                (json-read-file index-file)
              '()))
      (dolist (entry list)
        ;; Remove current entry
        (setq data (seq-remove (lambda (o)
                                 (and (string-match (regexp-quote (alist-get 'source o)) (alist-get 'source entry))
                                      (string= (alist-get 'term o) (alist-get 'term entry))))
                               data))
        ;; Add a new entry
        (push
         `((term . ,(alist-get 'term entry))
           (icon . ,(alist-get 'icon entry))
           (source . ,(alist-get 'source entry)))
         data))
      (insert (json-encode (sort data :key (lambda (o) (alist-get 'term o)) :lessp #'string<))))))

(defun my-dot-grid-boxes-process (id &optional force)
  (interactive
   (list
    (my-complete-sketch-filename "drawing")
    current-prefix-arg))
  (let* ((labels (my-dot-grid-boxes-labels id))
         list)
    (cl-assert (= (% (length labels) 7) 0))
    (cl-assert (> (length labels) 1))
    (setq list
          (my-dot-grid-boxes-extract :output-dir my-sketch-icon-directory
                                     :num-rows (/ (length labels) 7)
                                     :filename (my-get-sketch-filename id)
                                     :labels labels
                                     :force force))
    (my-sketch-icon-update-index list)))

(defun my-dot-grid-boxes-process-all-icons ()
  (interactive)
  (dolist (source (my-sketches "icons")) (my-dot-grid-boxes-process source)))

Then I made a simple interface for looking up icons.

2025-10-01_10-09-32.png
Figure 2: Screencast showing my icon lookup interface

I can filter it by terms, and I can exclude the icons I've copied from illustration books for practice.

I can even use it as a rudimentary visual menu for showing A+ some choices.

Oatmeal, cereal, pancakes

2025-09-30_13-40-06.png

Reflections on doodling

My curves are shaky. I'm mostly learning to ignore that and draw anyway. Good thing redoing them is a matter of a two-finger tap with my left hand, and then I can redraw until it feels mostly right. I try up to three times before I say, fine, let's just go with that.

I often draw with my iPad balanced on my lap, so there's an inherent wobbliness to it. I think this is a reasonable trade-off. Then I can keep drawing cross-legged in the shade at the playground instead of sitting at the table in the sun. The shakiness is still there when I draw on a solid table, though. I have a Paperlike screen protector, which I like more than the slippery feel of the bare iPad screen. That helps a little.

It's possible to cover it up and pretend to confidence that I can't draw with. I could smooth out the shakiness of my curves by switching to Procreate, which has more stylus sensitivity settings than Noteful does. A+ loves the way Procreate converts her curves to arcs. She moves the endpoints around to where she wanted to put them. I'm tempted to do the same, but I see her sometimes get frustrated when she tries to draw without that feature, and I want to show her the possibilities that come with embracing imperfection. It's okay for these sketches to be a little shaky. These are small and quick. They don't have to be polished.

The Internet says to draw faster and with a looser grip, and that lots of practice will build fine motor skills. I'm not sure I'll get that much smoother. I think of my mom and her Parkinson's tremors, and I know that time doesn't necessarily bring improvement. But it's better to keep trying than to shy away from it. Maybe as I relax more into having my own time, working on my own things and moving past getting things done, I'll give myself more time for drawing exercise, like filling pages with just lines and circles.

Reflections on sources

I had fun coming up with words and drawing them. I could start with whatever was in front of me and go from there. I used my phone to look up the occasional reference image, like the heart. Sometimes A+ suggested things to draw. Sometimes she even took over.

The books were handy when I didn't feel like thinking much. I could just reproduce the already-simplified drawings. I often felt like I still wanted to tweak things a bit more to make them feel like my own, though, which was a useful way to figure out more about what I like.

Instead of mimicking other people's sketches, I can mine my sketchnotes and pull out the concepts I tend to think about a lot. If I've drawn them in Noteful, I can even copy them from their original sketches, resize them, and make the lines a consistent thickness. If I've drawn them elsewhere, it's easy enough to redraw.

Next steps

I think I'll keep drawing these visual vocabulary practice sketches, focusing more on my own ways of drawing. It's fun. I have 324 icons at the moment. I wonder what the collection will be like when I have a thousand terms in it.

Ellane W's mustache post reminded me that Inktober has just started, so that's another source of ideas. Zsolt's 2021 post on sketchnoting for PKM led me to Quick, Draw!, which has a data set of 50 million images based on 345 simple drawing prompts. There's also a TU Berlin dataset with 250 drawing prompts; the original website doesn't seem to be around any more, but I can get the drawing prompts from the README on HuggingFace. SketchDaily Reference Site could be good for randomness and inspiration, too.

Building a visual library is a great way to learn how to actually draw things. I'm curious about using this 30-minute drawing exercise to start paying attention to a few things, and maybe using the shrimp method if there's something I really want to nail down. Visual mnemonic links might be a way to explore the connections between things as I wander around ideas (even though this video is way more advanced than I am).

On the Emacs side, it might be interesting to quickly add a related doodle to the margin of a blog post, or to look up or copy a personal reference image as I untangle my thoughts in a sketch. I'm tempted to write some Emacs Lisp that searches for these terms in my draft blog posts and adds a little hint whenever it finds a match. Another small piece of code might identify recurring nouns and verbs in recent posts and suggest those if I haven't drawn them yet. Could be fun.

Anyway, check out my icon library if you want!

View org source for this post

Org Mode: calculating table sums using tag hierarchies

| org, elisp

While collecting posts for Emacs News, I came across this question about adding up Org Mode table data by tag hierarchy, which might be interesting if you want to add things up in different combinations. I haven't needed to do something like that myself, but I got curious about it. It turns out that you can define a tag hierarchy like this:

#+STARTUP: noptag
#+TAGS:
#+TAGS: [ GT1 : tagA tagC tagD ]
#+TAGS: [ GT2 : tagB tagE ]
#+TAGS: [ GT3 : tagB tagC tagD ]

The first two lines remove any other tags you've defined in your config aside from those in org-tag-persistent-alist, but can be omitted if you want to also include other tags you've defined in org-tag-alist. Note that it doesn't have to be a strict tree. Tags can belong to more than one tag group.

EduMerco wanted to know how to use those tag groups to sum up rows in a table. I added a #+NAME header to the table so that I could refer to it with :var source=source later on.

#+NAME: source
| tag  | Q1 | Q2 |
|------+----+----|
| tagA |  9 |    |
| tagB |  4 |  2 |
| tagC |  1 |  4 |
| tagD |    |  5 |
| tagE |    |  6 |
(defun my-sum-tag-groups (source &optional groups)
  "Sum up the rows in SOURCE by GROUPS.
If GROUPS is nil, use `org-tag-groups-alist'."
  (setq groups (or groups org-tag-groups-alist))
  (cons
   (car source)
   (mapcar
    (lambda (tag-group)
      (let ((tags (org--tags-expand-group (list (car tag-group))
                                          groups nil)))
        (cons (car tag-group)
              (seq-map-indexed
               (lambda (colname i)
                 (apply '+
                        (mapcar (lambda (tag)
                                  (let ((val (or (elt (assoc-default tag source) i) "0")))
                                    (if (stringp val)
                                        (string-to-number val)
                                      (or val 0))))
                                tags)))
               (cdr (car source))))))
    groups)))

Then that can be used with the following code:

#+begin_src emacs-lisp :var source=source :colnames no :results table
(my-sum-tag-groups source)
#+end_src

to result in:

tag Q1 Q2
GT1 10 9
GT2 4 8
GT3 5 11

Because org--tags-expand-group takes the groups as a parameter, you could use it to sum things by different groups. The #+TAGS: directives above set org-tag-groups-alist to:

(("GT1" "tagA" "tagC" "tagD")
 ("GT2" "tagB" "tagE")
 ("GT3" "tagB" "tagC" "tagD"))

Following the same format, we could do something like this:

(my-sum-tag-groups source '(("Main" "- Subgroup 1" "- Subgroup 2")
                            ("- Subgroup 1" "tagA" "tagB")
                            ("- Subgroup 2" "tagC" "tagD")
                            ))
tag Q1 Q2
Main 14 11
- Subgroup 1 13 2
- Subgroup 2 1 9

I haven't specifically needed to add tag groups in tables myself, but I suspect the recursive expansion in org--tags-expand-group might come in handy even in a non-Org context. Hmm…

View org source for this post

2025-09-29 Emacs news

| emacs, emacs-news

Very niche, but I'm happy to see that nethack-el is actively being worked on again. I remember having a lot of fun with that. =)

Also, the theme for October's Emacs Carnival is maintenance. Check out the posts for September's theme of obscure packages, too!

Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, Mastodon #emacs, Bluesky #emacs, Hacker News, lobste.rs, programming.dev, lemmy.world, lemmy.ml, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, and emacs-devel. Thanks to Andrés Ramírez for emacs-devel links. Do you have an Emacs-related link or announcement? Please e-mail me at sacha@sachachua.com. Thank you!

View org source for this post

Org Mode: a LaTeX letter that includes PDFs and hyperlinked page numbers

| org

I messed up on one of my tax forms, so I needed to send the tax agency a single document that included the amended tax return and the supporting slips, with my name, social insurance number, and reference number on every page. It turned out to be rather complicated trying to get calculated \pageref to work with \includepdf, so I just used \hyperlink with hard-coded page numbers. I also needed to use qpdf --decrypt input.pdf output.pdf to decrypt a PDF I downloaded from one of my banks before I could include it with \includepdf.

Here's what I wanted to do with this Org Mode / LaTeX example:

  • Coloured header on all pages with info and page numbers
  • Including PDFs
  • Hyperlinks to specific pages
* Letter
#+DATE: 2025-09-24
#+LATEX_CLASS: letter
#+OPTIONS: toc:nil ^:nil title:nil
#+LATEX_HEADER: \usepackage[margin=1in]{geometry}
#+LATEX_HEADER: \hypersetup{hidelinks}
#+LATEX_HEADER: \usepackage{pdfpages}
#+LATEX_HEADER: \usepackage{fancyhdr}
#+LATEX_HEADER: \usepackage{lastpage}
#+LATEX_HEADER: \usepackage{xcolor}
#+LATEX_HEADER: \signature{FULL NAME GOES HERE}
#+LATEX_HEADER: \fancypagestyle{plain}{
#+LATEX_HEADER: \fancyhf{}
#+LATEX_HEADER: \fancyhead[L]{\color{teal}\hyperlink{page.1}{HEADER INFO}}
#+LATEX_HEADER: \fancyhead[R]{\color{teal}\thepage\ of \pageref{LastPage}}
#+LATEX_HEADER: }
#+LATEX_HEADER: \pagestyle{plain}
#+LATEX_HEADER: \makeatletter
#+LATEX_HEADER: \let\ps@empty\ps@plain
#+LATEX_HEADER: \let\ps@firstpage\ps@plain
#+LATEX_HEADER: \makeatother
#+LATEX_HEADER: \renewcommand{\headrulewidth}{0pt}
#+LATEX_HEADER: \newcommand{\pdf}[1]{\includepdf[link,pages=-, scale=.8]{#1}}
#+LATEX_HEADER: \newcommand{\pages}[2]{\hyperlink{page.#1}{#1}-\hyperlink{page.#2}{#2}}
#+LATEX: \begin{letter}{}
#+LATEX: \opening{Dear person I am writing to:}

Text of the letter goes here.
Please find attached:

| Pages                             | |
| @@latex:\pages{2}{10}@@           | Description of filename1.pdf |
| @@latex:\hyperlink{page.5}{5}@@ | Can link to a specific page |
| @@latex:\pages{11}{15}@@           | Description of filename2.pdf |

#+LATEX:\closing{Best regards,}

#+LATEX: \end{letter}

#+LATEX: \pdf{filename1.pdf}
#+LATEX: \pdf{filename2.pdf}

After filling it in, I exported it with C-c C-e (org-export) C-s (to limit it to the subtree) l p (to export a PDF via LaTeX).

Not the end of the world. At least I learned a little more LaTeX and Org Mode along the way!

View org source for this post

Visual vocabulary practice - ABCs

| drawing

I've been giving myself more time to just enjoy drawing: not trying to untangle a thought, just wandering around and seeing where the lines and colours take me. While looking for examples of sketchnotes for self-facilitation for my post on finding the shape of my thoughts, I came across Sketchnotes: Changing The Way You See Your Thoughts — Creative Soul of Denise Nicole. I liked the ABC exercise near the bottom. For fun, I copied the same words and tried my own spin on things.

Text from sketch

Visual vocabulary practice - ABCs 2025-09-21-04

  • anchor
  • banner
  • calculate
  • DNA
  • energy
  • freeze
  • guitar
  • height
  • Instagram
  • judge
  • kid
  • ladder
  • meeting (A+ drew the details)
  • network
  • obstacle
  • planning
  • quote
  • repeat
  • scroll
  • think
  • universe
  • volley
  • weight
  • x-ray
  • yo-yo
  • zoo
  • extra: jar
  • extra: light

When A+ saw what I was doing, she asked me to swap out my meeting icon from "people around a table" to her online meetings at virtual school. She even added details: "This is the kid with the Minecraft background, this is the kid with big headphones…" I enjoyed watching her in this state of playful focus. I wonder what else I can draw that she might have fun taking over. "Meeting" is my favourite one in this set, but since that's mostly A+'s, my next favourite is "freeze." Canada gets even colder than -20C, but for me, -20C is definitely stay inside weather.

I've also been enjoying Kamo's books, like How to Draw Cute Doodles and Illustrations. Her style reminds me of the Illustration School series by Sachiko Umoto, which I also liked. I think I tend towards simple and approachable rather than realistic or technically impressive. Learning how to draw concrete things might help me get better at drawing abstract things. It's fun to slow down and pay attention to more details, too. Turns out I'd been drawing guitar holes in the wrong place all this time. Now I know!

Related posts:

Here are some other resources that might be helpful:

View org source for this post

EmacsConf infrastructure upgrades

| emacsconf

With the EmacsConf call for proposals now closed, I have a little time before EmacsConf speakers send in their pre-recorded videos come in for captioning. I decided to dust off the infrastructure to see what makes sense to upgrade.

We use Etherpad for collaborative note-taking during EmacsConf. It's straightforward to use and pretty reliable. Conference participants can use it to share notes, questions, and links. They can also use IRC to ask questions, and volunteers copy those questions into the pad for the talk. Hosts and speakers can keep an eye on the pad for questions. We send speakers a copy of their talk's pad after the conference, and we post that along with other follow-up questions on the conference wiki. Here's an example: Writing academic papers in Org-Roam.

A native Emacs solution for collaborative notes would be even better. CRDT was great for experimenting with real-time collaboration within Emacs, but I'm not sure it can handle a ton of simultaneous connections and I don't want to find out in the middle of the conference. Also, requiring Emacs would leave out the people who only have a web browser handy. It would be super cool if we had something with Emacs, web, and IRC interfaces, but for now, Etherpad will do.

We started by using Wikimedia's instance, and then we moved to hosting our own. For the past two years, we've used Etherpad 1.9.7. Etherpad is currently at version 2.5.0. There are some performance improvements, bugfixes, and security fixes, so I think it'll be worth upgrading to that. I don't know of any specific issues or upgrades, but it's a good idea to stay closer to the latest release than to get too far out of date.

I switched our roles/pad/tasks/main.yml to use systemli.etherpad from ansible-galaxy. I also figured out how to set up a Vagrant virtual machine that I could destroy and reconfigure with vagrant destroy; vagrant up --provision. Here's my Vagrant file for that:

# -*- mode: ruby -*-
Vagrant.configure("2") do |config|
  config.vm.box = "debian/bookworm64"
  config.vm.define "pad" do |pad|
  end
  config.vm.provider "virtualbox" do |vb|
    vb.memory = "2048"
  end
  config.vm.network "private_network", ip: "192.168.56.2"
  config.vm.provision "ansible" do |ansible|
    ansible.playbook = "../../vagrant-playbook.yml"
  end
end

The playbook it refers to has this:

- name: Pre-flight checks and package installation
  hosts: pad
  become: true
  gather_facts: false
  pre_tasks:
    - name: Ensure ntpdate is installed for time sync
      ansible.builtin.apt:
        name: ntpdate
        state: present
        update_cache: yes
    - name: Synchronize system clock
      ansible.builtin.command: ntpdate pool.ntp.org
      changed_when: true
    - name: Ensure ACL package is installed
      ansible.builtin.apt:
        name: acl
        state: present
- name: Load vars
  hosts: pad
  tags: always
  tasks:
    - include_vars:
        file: vagrant-vars.yml
- name: Set up pad proxy
  hosts: pad
  tags: proxy
  roles:
    - pad-proxy
- name: Set up pad
  hosts: pad
  tags: pad
  roles:
    - pad

I had used Vagrant in 2013, and it felt good to have the time to set up more testing infrastructure. I liked being able to test my pad and pad-proxy roles against a local virtual machine. I could figure out whatever tweaks I needed without messing up the production instance that we use for some meetups in between conferences.

Our production instance is on Debian 10 (Buster). That has reached its end of life for security updates, so apt-cache update doesn't work on it any more, and those steps in my Ansible playbook fail. I'm waiting for Amin Bandali to work on upgrading that server, since he has other stuff running on it. By setting update_cache variable that I override in my inventory.yml and referring to it with update_cache: "" in my task, I can conditionally disable the apt-cache update steps. That let me run the playbook against the production server, and now we're on Etherpad 2.x.

I recently updated our BigBlueButton instance to version 3.0.12. We've decided to stay with Icecast 2.4.4-1 for doing the livestreaming. We'll probably also keep OBS 29.1.2 and ffmpeg 6.0.1 instead of upgrading. With no must-have new features and other organizers' limited availability, it's better to keep those parts stable. This year, we'll continue using whisperx to help with the first draft of captions, but we'll probably try large-v3 instead of large-v2 by default. Some people find that large-v3's performance is better, some people find it's worse, so we'll see. Now that I know about whisperx's --initial_prompt option, I might be able to nudge it to the vocabulary and punctuation style we like.

Since the bones seem pretty solid, I'm looking forward to refamiliarizing myself with the Emacs Lisp code I wrote to help run the conference. I saved a bunch of improvement ideas from last year, and I can't wait to turn them into code. That's probably going to be lots of fun!

View org source for this post

Emacs: Cycle through different paragraph formats: all on one line, wrapped, max one sentence per line, one sentence per line

Posted: - Modified: | emacs

: Add move-to-left-margin to work around bug when using fill-paragraph-semlf at the end of a paragraph.

I came across Schauderbasis - reformat paragraph via @EFLS@mastodon.social. Now I want M-q to cycle through different ways of wrapping text:

  • all on one line
  • according to fill-column
  • at most one sentence per line (although still wrapping at fill-column)
  • at most one sentence per line (don't even try to keep it within fill-column).

Screencast cycling through different paragraph formats

Now that semantic linefeeds are part of core Emacs (as of 2025-06-14), the code for cycling through different paragraph formats can be pretty short. Most of it is actually just the logic for cycling through different commands. That might come in handy elsewhere. There's an unfill package as well, but since the code for unfilling a paragraph is very simple, I'll just include that part.

Note that fill-paragraph-semlf pays attention to sentence-end-double-space, and it doesn't handle comments yet. I also have some code to check if I'm in a comment and skip those filling methods if needed.

This might encourage me to write shorter sentences. I can move sentences around with M-Shift-up and M-Shift-down in Org Mode, which is pretty handy. Also, one sentence per line makes diffs easier to read. But wrapped text is annoying to edit in Orgzly Revived on my phone, because the wrapping makes a very ragged edge on a narrow screen. I might unwrap things that I want to edit there. With a little bit of tweaking to skip source blocks, I can narrow to the subtree, select my whole buffer, and cycle the formatting however I like.

(defvar my-repeat-counter '()
  "How often `my-repeat-next' was called in a row using the same command.
This is an alist of (cat count list) so we can use it for different functions.")

(defun my-unfill-paragraph ()
  "Replace newline chars in current paragraph by single spaces.
This command does the inverse of `fill-paragraph'."
  (interactive)
  (let ((fill-column most-positive-fixnum))
    (fill-paragraph)))

(defun my-fill-paragraph-semlf-long ()
  (interactive)
  (let ((fill-column most-positive-fixnum))
    (fill-paragraph-semlf)))

(defun my-repeat-next (category &optional element-list reset)
  "Return the next element for CATEGORY.
Initialize with ELEMENT-LIST if this is the first time."
  (let* ((counter
          (or (assoc category my-repeat-counter)
              (progn
                (push (list category -1 element-list)
                      my-repeat-counter)
                (assoc category my-repeat-counter)))))
    (setf (elt (cdr counter) 0)
          (mod
           (if reset 0 (1+ (elt (cdr counter) 0)))
           (length (elt (cdr counter) 1))))
    (elt (elt (cdr counter) 1) (elt (cdr counter) 0))))

(defun my-in-prefixed-comment-p ()
  (or (member 'font-lock-comment-delimiter-face (face-at-point nil t))
      (member 'font-lock-comment-face (face-at-point nil t))
      (save-excursion
        (beginning-of-line)
        (comment-search-forward (line-end-position) t))))

;; It might be nice to figure out what state we're
;; in and then cycle to the next one if we're just
;; working with a single paragraph. In the
;; meantime, just going by repeats is fine.
(defun my-reformat-paragraph-or-region ()
  "Cycles the paragraph between three states: filled/unfilled/fill-sentences.
If a region is selected, handle all paragraphs within that region."
  (interactive)
  (let ((func (my-repeat-next 'my-reformat-paragraph
                              '(fill-paragraph my-unfill-paragraph fill-paragraph-semlf
                                               my-fill-paragraph-semlf-long)
                              (not (eq this-command last-command))))
        (deactivate-mark nil))
    (if (region-active-p)
        (save-restriction
          (save-excursion
            (narrow-to-region (region-beginning) (region-end))
            (goto-char (point-min))
            (while (not (eobp))
              (skip-syntax-forward " ")
              (let ((elem (and (derived-mode-p 'org-mode)
                               (org-element-context))))
                (cond
                 ((eq (org-element-type elem) 'headline)
                  (org-forward-paragraph))
                 ((member (org-element-type elem)
                          '(src-block export-block headline property-drawer))
                  (goto-char
                   (org-element-end (org-element-context))))
                 (t
                  (funcall func)
                  (if fill-forward-paragraph-function
                      (funcall fill-forward-paragraph-function)
                    (forward-paragraph))))))))
      (save-excursion
        (move-to-left-margin)
        (funcall func)))))

(keymap-global-set "M-q" #'my-reformat-paragraph-or-region)

Sometimes I use writeroom-mode to make the lines look even narrower, with lots of margin on the side.

Related:

This is part of my Emacs configuration.
View org source for this post