Sacha Chua https://sachachua.com/blog/feed/ Emacs, sketches, and life Mon, 06 Feb 2023 14:36:50 GMT en-US daily 1 11ty 2023-02-06 Emacs news https://sachachua.com/blog/2023/02/2023-02-06-emacs-news/ Mon, 06 Feb 2023 14:36:50 GMT emacs emacs-news https://sachachua.com/blog/2023/02/2023-02-06-emacs-news/
  • Upcoming events:
  • Beginner:
  • Emacs Lisp:
  • Navigation:
  • Dired:
  • Org Mode:
  • Completion:
  • Coding:
  • Shells:
  • Web:
  • Fun:
  • Community:
  • Other:
  • New packages:
    • ekg: A system for recording and linking information (MELPA)
    • flycheck-eglot: Flycheck support for eglot (MELPA)
    • jet: Emacs integration for jet Clojure tool (MELPA)
    • kotlin-ts-mode: A mode for editing Kotlin files based on tree-sitter (MELPA)
    • langtool-popup: Popup message extension for langtool.el (MELPA)
    • sas-py: SAS with SASPy (MELPA)
    • timu-macos-theme: Color theme inspired by the macOS UI (MELPA)
  • Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. 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!

    ]]>
    Using Org Babel to learn Rubik's cube algorithms https://sachachua.com/blog/2023/02/using-org-babel-to-learn-rubik-s-cube-algorithms/ Thu, 02 Feb 2023 14:43:39 GMT emacs cubing org https://sachachua.com/blog/2023/02/using-org-babel-to-learn-rubik-s-cube-algorithms/ A+ has started learning Rubik's cube algorithms for permutation of the last layer (PLL) algorithms for the Rubik's cube. To help her focus on just a few at a time instead of getting distracted by the long list in the Cubeskills PLL PDF, I made a page that listed the algorithms that she was working on so that I could export it with ox-hugo to the mini-site I made for her interests.

    She sometimes gets a little confused about clockwise and counter-clockwise, so I used Roofpig to add an animated view of the algorithm that she can step through. I wanted to make it easier for her to follow the algorithm without constantly moving her hands from the cube to the tablet or looking up and down all the time, but I didn't want to recompile the source just yet. I used the roofpig_and_three.min.js file and hacked speech synthesis into it by modifying the object prototype. For example, here's the Org source for adding a Jb perm:

    #+begin_export html
    <div class="roofpig" data-config="base=PLL|alg=R U R' F' R U R' U' R' F R2 U' R' U'"></div>
    #+end_export
    

    and here's what it looks like. Might only look nice on my website, and I added speech synthesis so you may want to mute it if you need to be quiet.

    Code for setting up Roofpig with speech synthesis
    <!--  -*- mode: web -*- -->
    <script defer src="/blog/2023/02/using-org-babel-to-learn-rubik-s-cube-algorithms/roofpig_and_three.min.js"></script>
    <style>
     .roofpig { max-width: 400px; margin-bottom: 80px; }
     </style>
    <script>
     function setUpCubes() {
       if (window.cubesSpeakMoves || !window.speechSynthesis) return;
       if (window.CubeAnimation) {
         addSpeechToCubeAnimations();
         window.cubesSpeakMoves = true;
       } else {
         setTimeout(setUpCubes, 300);
       }
     }
     
     function addSpeechToCubeAnimations() {
       if (!window.CubeAnimation || !window.CubeAnimation['by_id'] || !window.CubeAnimation['by_id'][1]) return;
       var cachedFunc = Object.getPrototypeOf(CubeAnimation['by_id'][1].dom).alg_changed;
       Object.getPrototypeOf(CubeAnimation['by_id'][1].dom).alg_changed = function() {
         if (arguments[4].past.length > lastNoted.length) {
           let moves = arguments[4].past.split(' ');
           let lastMove = moves[moves.length - 1];
           // is it lower-case? speak lowercase explicitly
           if (lastMove.match(/[rludbf]/)) {
             lastMove = 'lower ' + lastMove;
           } else { // avoid awkward-sounding moves like "capital R"
             lastMove = lastMove.toLowerCase();
           }
           lastMove = lastMove.replace(/'/, ' prime');
           window.speechSynthesis.speak(new SpeechSynthesisUtterance(lastMove));
         } else {
           console.log('going backwards');
         }
         lastNoted = arguments[4].past;
         return cachedFunc.apply(this, arguments);
       }
     }
     ROOFPIG_CONF_F2L = "solved=U*|hover=none|colored=U-|flags=canvas,showalg|speed=1000";
     ROOFPIG_CONF_PLL = "solved=U-|hover=near|colored=U*|flags=canvas,showalg|speed=1000";
     var lastNoted = '';
     setUpCubes();
    </script>
    

    I also wanted to include diagrams to make it easier for her to choose the right algorithm and position the cube the right way at the beginning, but I didn't want to fuss around with lots of screenshots and little files. It turns out you can define arrows in SVG pretty easily, so I wrote some Emacs Lisp functions to generate those types of diagrams. First I started with just the arrows.

    (my-cubing-last-layer-arrows '((2 8 t) (5 7 t)))
    
    arrows.svg

    For practising recognition, I wanted to also include the colors on top and on the sides:

    (my-cubing-last-layer-with-sides "OOGRROGGRBBB" "YYYYYYYYY" '((2 8 t) (5 7 t)))
    
    colors.svg

    Emacs Lisp functions for cubing diagrams
    ;; Start of cubing code
    (defun my-cubing-pos (size n i)
      (list
       (* (/ size n) (% i n))
       (* (/ size n) (/ i n))))
      
    (defun my-cubing-last-layer-arrows (arrows)
      "Draw ARROWS.
    Arrows are defined as a list of lists of the form
    ((from to) (from to t) ...). Ex: '(my-cubing-last-layer-arrows '((3 1 t) (2 8 t)))
    Cells are numbered from left to right, top to bottom, with the top left box being 0.
    "
      (let* ((size 99)
             (n 3)
             (arrow-color "#000")
             (svg (svg-create size size)))
        (svg--append
         svg
         (dom-node
          'defs
          nil
          (dom-node
           'marker
           '((id . "arrowhead")
             (markerWidth . "10")
             (markerHeight . "7")
             (refX . "0")
             (refY . "3.5")
             (orient . "auto-start-reverse"))
           (dom-node
            'polygon
            `((fill . ,arrow-color)
              (points . "0 0, 4 3.5, 0 7")))
           )))
        (dotimes (i (* n n))
          (let ((pos (my-cubing-pos size n i)))
            (svg-rectangle
             svg
             (car pos)
             (cadr pos)
             (/ size n)
             (/ size n)
             :fill "#fff"
             :stroke-width 1
             :stroke "#666")))
        (dolist (arrow arrows)
          (let ((from (car arrow))
                (to (cadr arrow)))
            (apply 'svg-line
                   (append
                    (list svg)
                    (mapcar (lambda (o) (+ o (/ size (* 2 n))))
                            (my-cubing-pos size n from))
                    (mapcar (lambda (o) (+ o (/ size (* 2 n))))
                            (my-cubing-pos size n to))
                    (list
                     :stroke-width 2
                     :stroke arrow-color
                     :marker-start (if (elt arrow 2) "url(#arrowhead)")
                     :marker-end "url(#arrowhead)")))))
        (with-temp-buffer
          (svg-print svg)
          (buffer-string))))
    
    (defvar my-cubing-colors '((?R  . "#ff0000")
                               (?G  . "#00ff00")
                               (?B  . "#0000ff")
                               (?O  . "#ed7117")
                               (?Y  . "#ffff00")
                               (?W  . "#ffffff")
                               (?\? . "#666666")))
    
    (defun my-cubing-last-layer-with-sides (sides top arrows)
      "Draw a diagram of the top of the cube.
    The style is similar to https://www.cubeskills.com/uploads/pdf/tutorials/pll-algorithms.pdf .
    SIDES is a string specifying colors going clockwise from the back-left side.
    TOP is a string specifying colors going from left to right, top to bottom.
    Arrows are defined as a list of lists of the form ((from to) (from to t) ...).
    Cells are numbered from left to right, top to bottom, with the top left box being 0.
    Ex: (my-cubing-last-layer-with-sides \"ORRBOOGGGRBB\" \"YYYYYYYYY\" '((3 1 t) (2 8 t)))
    "
      (let* ((size 99)
             (n 3)
             (side-size 10)
             (cell-size (/ (- size (* 2 side-size)) n))
             (arrow-color "#000")
             (svg (svg-create size size)))
        (svg--append
         svg
         (dom-node
          'defs
          nil
          (dom-node
           'marker
           '((id . "arrowhead")
             (markerWidth . "10")
             (markerHeight . "7")
             (refX . "0")
             (refY . "3.5")
             (orient . "auto-start-reverse"))
           (dom-node
            'polygon
            `((fill . ,arrow-color)
              (points . "0 0, 4 3.5, 0 7"))))))
        ;; Draw the sides. It's a string of colors going clockwise from back left
        (when sides
          (dotimes (i (* n 4))
            (apply 'svg-rectangle
                   (append
                    (list svg)
                    (pcase (/ i n)
                      (0 (list (+ (* (% i n) cell-size) side-size)
                               0
                               cell-size
                               side-size))
                      (1 (list (+ side-size (* n cell-size))
                               (+ (* (% i n) cell-size) side-size)
                               side-size
                               cell-size))
                      (2 (list (+ (* (- n (% i n) 1) cell-size) side-size)
                               (+ (* n cell-size) side-size)
                               cell-size
                               side-size))
                      (3 (list 0
                               (+ (* (- n (% i n) 1) cell-size) side-size)
                               side-size
                               cell-size)))
                    (list
                     :stroke-width 1
                     :stroke "#666"
                     :fill (assoc-default (elt sides i)
                                          my-cubing-colors
                                          'eq
                                          (assoc-default ?\? my-cubing-colors)))))))
        ;; Draw the top face specified by a string of colors going from left to right, top to bottom
        (dotimes (i (* n n))
          (let ((pos (my-cubing-pos (* cell-size n) n i)))
            (svg-rectangle
             svg
             (+ side-size (car pos))
             (+ side-size (cadr pos))
             cell-size
             cell-size
             :fill (if top
                       (assoc-default (elt top i) my-cubing-colors
                                      'eq
                                      (assoc-default ?\? my-cubing-colors))
                     (assoc-default ?\? my-cubing-colors))
             :stroke-width 1
             :stroke "#666")))
        ;; Draw the arrows
        (dolist (arrow arrows)
          (let ((from (car arrow))
                (to (cadr arrow)))                  
            (apply 'svg-line
                   (append
                    (list svg)
                    (mapcar (lambda (o) (+ side-size o (/ cell-size 2)))
                            (my-cubing-pos (* n cell-size) n from))
                    (mapcar (lambda (o) (+ side-size o (/ cell-size 2)))
                            (my-cubing-pos (* n cell-size) n to))
                    (list
                     :stroke-width 2
                     :stroke arrow-color
                     :marker-start (if (elt arrow 2) "url(#arrowhead)")
                     :marker-end "url(#arrowhead)")))))
        (with-temp-buffer
          (svg-print svg)
          (buffer-string))))
    ;; end of cubing code
    

    I'll probably need to tweak the arrows when we eventually get to the G perms, but we're still a long way off. And it would probably be pretty cool to be able to generate the colours by going backwards from the algorithm, maybe building on top of emacs-cube, so that I can write my own notes about recognizing the in-between steps and recovering from the typical mistakes we make. (That wasn't around the last time I wrote about Emacs and cubing. Thanks to Akib for making and sharing it!) I'm curious about this LaTeX approach, too, but that can wait for another day.

    ]]>
    Using Spookfox to scroll Firefox up and down from Emacs https://sachachua.com/blog/2023/01/using-spookfox-to-scroll-firefox-up-and-down-from-emacs/ Mon, 30 Jan 2023 16:02:57 GMT web emacs https://sachachua.com/blog/2023/01/using-spookfox-to-scroll-firefox-up-and-down-from-emacs/ I open lots of pages in the process of making Emacs News. I like to open the pages in Mozilla Firefox, but I want the keyboard focus to stay with Emacs so that I can quickly categorize the links. I also sometimes want to scroll the page up or down. While reading the Reading, and not forgetting post, I came across Spookfox, which bridges Emacs and Firefox using an Firefox add-on and websockets. After I started spookfox and connected to it by clicking on the extension in Firefox, I was able to interact with it from Emacs Lisp. I feel a little nervous about it security-wise, but at least it's only listening on the local port. There might be another way to do it with the Marionette support in Firefox, but I haven't looked into it yet.

    (use-package spookfox
      :quelpa (spookfox :fetcher github :repo "bitspook/spookfox"
                        :files ("lisp/*.el" "lisp/apps/*.el"))
      :when my-laptop-p
      :config
      (require 'spookfox-tabs)
      (require 'spookfox-org-tabs)
      (require 'spookfox-js-injection)
      (add-to-list 'spookfox-enabled-apps 'spookfox-tabs)
      (add-to-list 'spookfox-enabled-apps 'spookfox-org-tabs)
      (add-to-list 'spookfox-enabled-apps 'spookfox-js-injection)
      ;; (spookfox-init) ; don't automatically enable it; run (spookfox-init) to manually enable
      )
    

    Anyway, this code seems to do the job of scrolling my Firefox window:

    (defun my-spookfox-scroll-down ()
      (interactive)
      (spookfox-eval-js-in-active-tab "window.scrollBy(0, document.documentElement.clientHeight);"))
    
    (defun my-spookfox-scroll-up ()
      (interactive)
      (spookfox-eval-js-in-active-tab "window.scrollBy(0, -document.documentElement.clientHeight);"))
    
    (global-set-key (kbd "C-s-v") 'my-spookfox-scroll-down)
    (global-set-key (kbd "C-s-S-v") 'my-spookfox-scroll-up)
    

    This code opens a tab without switching keyboard focus away from Emacs:

    (defun my-spookfox-background-tab (url &rest args)
      "Open URL as a background tab."
      (if spookfox--connected-clients
          (spookfox-tabs--request (cl-first spookfox--connected-clients) "OPEN_TAB" `(:url ,url))
        (browse-url url)))
    

    My Emacs News code for processing my upvoted Reddit posts can automatically grab the links from Reddit link posts, but sometimes people post Reddit text or image posts and then include the link to the actual project in the post body or a comment instead.

    (defun my-spookfox-get-links ()
      (seq-uniq
       (spookfox-eval-js-in-active-tab "[...(document.querySelector('[data-testid=post-container]')?.parentElement || document).querySelectorAll('a')].map(a => a.href).filter(a => a && !a.match(/redd\.?it/) && !a.match(window.location.host))" t)))
    ;;https://emacs.stackexchange.com/questions/41801/how-to-stop-completing-read-ivy-completing-read-from-sorting
    (defun my-presorted-completion-table (completions)
      (lambda (string pred action)
        (if (eq action 'metadata)
            '(metadata
             (cycle-sort-function . identity)
             (display-sort-function . identity))
          (complete-with-action action completions string pred))))
    
    (defun my-spookfox-insert-link-from-page ()
      (interactive)
      (let* ((links (my-spookfox-get-links))
             (link (completing-read
                    "Link: "
                    (my-presorted-completion-table
                     links))))
        (insert (org-link-make-string link (my-page-title link)))))
    
    This is part of my Emacs configuration.
    ]]>
    2023-01-30 Emacs news https://sachachua.com/blog/2023/01/2023-01-30-emacs-news/ Mon, 30 Jan 2023 13:48:36 GMT emacs emacs-news https://sachachua.com/blog/2023/01/2023-01-30-emacs-news/
  • Upcoming events:
  • Beginner:
  • Emacs configuration:
  • Emacs Lisp:
  • Appearance:
  • Navigation:
  • Dired:
  • Org Mode:
  • Denote:
  • Coding:
  • Math:
  • Shells:
  • Mail, news, and chat:
  • Multimedia:
  • Fun:
  • AI:
  • Community:
  • Other:
  • Emacs development:
  • New packages:
    • empv: An interface for MPV (MELPA)
    • file-info: Show pretty information about current file (MELPA)
    • flymake-cspell: A Flymake backend for CSpell (MELPA)
    • flymake-ruff: A flymake plugin for python files using ruff (MELPA)
    • gpr-mode: Major mode for editing GNAT project files (GNU ELPA)
    • ibrowse: Interact with your browser (MELPA)
    • mic: Minimal and combinable configuration manager (MELPA)
    • poke: Emacs meets GNU poke! (GNU ELPA)
    • smilefjes: View Norwegian Food Safety Authority restaurant ratings (MELPA)
    • treesit-auto: Automatically use tree-sitter enhacned modes, if available (MELPA)
  • Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. 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!

    ]]>
    Checking image sizes and aspect ratios in Emacs Lisp so that I can automatically smartcrop them https://sachachua.com/blog/2023/01/checking-image-sizes-and-aspect-ratios-in-emacs-lisp-so-that-i-can-automatically-smartcrop-them/ Mon, 30 Jan 2023 00:29:43 GMT emacs https://sachachua.com/blog/2023/01/checking-image-sizes-and-aspect-ratios-in-emacs-lisp-so-that-i-can-automatically-smartcrop-them/ A+ occasionally likes to flip through pictures in a photo album. I want to print another batch of 4x6 photos, and I'd like to crop them before labeling them with the date from the EXIF info. Most of the pictures are from my phone, so I have a 4:3 aspect ratio instead of the 3:2 aspect ratio I want for prints.

    First step: figuring out how to get the size of an image. I could either use Emacs's built-in image-size function or call ImageMagick's identify command. Which one's faster? First, I define the functions:

    (defun my-image-size (filename)
      (let ((img (create-image filename)))
        (prog1 (image-size img t) (image-flush img))))
    
    (defun my-identify-image-size (filename)
      (let ((result
             (seq-map 'string-to-number
                      (split-string
                       (shell-command-to-string
                        (concat "identify -format \"%w %h\" " (shell-quote-argument filename)))))))
        (when (and result (> (car result) 0))
          result)))
    

    and then benchmark them:

    (let ((filename "~/Downloads/Other prints/20230102_135059.MP.jpg")
          (times 10))
      (list (benchmark times `(my-image-size ,filename))
            (benchmark times `(my-identify-image-size ,filename))))
    

    Looks like ImageMagick's identify command is a lot faster. Now I can define a filter:

    Code for aspect ratios
    (defun my-aspect-ratio (normalize &rest args)
      "Return the aspect ratio of ARGS.
    If NORMALIZE is non-nil, return an aspect ratio >= 1 (width is greater than height).
    ARGS can be:
    - width height
    - a filename
    - a list of (width height)"
      (let (size width height result)
        (cond
         ((stringp (car args))
          (setq size (my-identify-image-size (car args)))
          (setq width (car size) height (cadr size)))
         ((listp (car args))
          (setq width (car (car args)) height (cadr (car args))))
         (t
          (setq width (car args) height (cadr args))))
        (when (and width height)
          (setq result (/ (* 1.0 width) height))
          (if (and normalize (< result 1))
              (/ 1 result)
            result))))
    
    (defun my-files-not-matching-aspect-ratio (print-width print-height file-list)
      (let ((target-aspect-ratio (my-aspect-ratio t print-width print-height)))
        (seq-filter
         (lambda (filename)
           (let ((image-ratio (my-aspect-ratio t filename)))
             (when image-ratio
               (> (abs (- image-ratio
                          target-aspect-ratio))
                  0.001))))
         file-list)))
    

    and I could use it like this to get a list of files that need to be cropped:

    (my-files-not-matching-aspect-ratio 4 6 (directory-files "~/Downloads/Other prints" t))
    

    … which is most of the pictures, so let's see if I can get smartcrop to automatically crop them as a starting point. I used npm install -g smartcrop-cli node-opencv to install the Node packages I needed, and then I defined these functions:

    Code for cropping
    (defvar my-smartcrop-image-command '("smartcrop" "--faceDetection"))
    
    (defun my-smartcrop-image (filename aspect-ratio output-file &optional do-copy)
      "Call smartcrop command to crop FILENAME to ASPECT-RATIO if needed.
    Write the result to OUTPUT-FILE.
    If DO-COPY is non-nil, copy files if they already have the correct aspect ratio."
      (when (file-directory-p output-file)
        (setq output-file (expand-file-name (file-name-nondirectory filename)
                                            output-file)))
      (let* ((size (my-identify-image-size filename))
             (image-ratio (my-aspect-ratio t size))
             new-height new-width
             buf)
        (when image-ratio
          (if (< (abs (- image-ratio aspect-ratio)) 0.01)
              (when do-copy (copy-file filename output-file t))
            (with-current-buffer (get-buffer-create "*smartcrop*")
              (erase-buffer)
              (setq new-width
                    (number-to-string
                     (floor (min
                             (car size)
                             (*
                              (cadr size)
                              (if (> (car size) (cadr size))
                                  aspect-ratio
                                (/ 1.0 aspect-ratio))))))
                    new-height
                    (number-to-string
                     (floor (min
                             (cadr size)
                             (/
                              (car size)
                              (if (> (car size) (cadr size))
                                  aspect-ratio
                                (/ 1.0 aspect-ratio)))))))
              (message "%d %d -> %s %s: %s" (car size) (cadr size) new-width new-height filename)
              (apply 'call-process
               (car
                my-smartcrop-image-command)
               nil t t
               (append
                (cdr my-smartcrop-image-command)
                (list
                 "--width"
                 new-width
                 "--height"
                 new-height
                 filename
                 output-file))))))))
    

    so that I could use this code to process the files:

    (let ((aspect-ratio (my-aspect-ratio t 4 6))
          (output-dir "~/Downloads/Other prints/cropped"))
      (mapc (lambda (file)
              (unless (file-exists-p (expand-file-name (file-name-nondirectory file) output-dir))
                (my-smartcrop-image file  aspect-ratio output-dir t)))
            (directory-files "~/Downloads/Other prints" t)))
    

    Then I can use Geeqie to review the cropped images and straighten or re-crop specific ones with Shotwell.

    It looks like smartcrop removes the exif information (including original date), so I want to copy that info again.

    for FILE in *; do exiftool -TagsFromFile "../$FILE" "-all:all>all:all" "exif/$FILE"; done
    

    And then finally, I can add the labels with this add-labels.py script, which I call with add-labels.py output-dir file1 file2 file3....

    add-labels.py: add the date to the lower left corner
    #!/usr/bin/python3
    import sys
    import PIL
    import PIL.Image as Image
    import PIL.ImageDraw as ImageDraw
    import PIL.ImageFont as ImageFont
    from PIL import Image, ExifTags
    import re
    import os
    
    # use: add-labels.py output-dir photo1 photo2 photo3
    PHOTO_DIR = "/home/sacha/photos/"
    OUTPUT_DIR = sys.argv[1]
    OUTPUT_WIDTH = 6
    OUTPUT_HEIGHT = 4
    OUTPUT_RATIO = OUTPUT_WIDTH * 1.0 / OUTPUT_HEIGHT
    font_fname = "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf"
    ALWAYS = True
    DO_ROTATE = False
    
    def label_image(filename):
        numbers = re.sub(r'[^0-9]', '', filename)
        img = Image.open(filename)
        exif = {
            PIL.ExifTags.TAGS[k]: v
            for k, v in img._getexif().items()        if k in PIL.ExifTags.TAGS
        }
        if DO_ROTATE:
            # Rotate image
            if exif['Orientation'] == 3:
                img = img.rotate(180, expand=True)
            elif exif['Orientation'] == 6:
                img = img.rotate(270, expand=True)
            elif exif['Orientation'] == 8:
                img = img.rotate(90, expand=True)    
        # Label
        time = exif['DateTimeOriginal'][0:10].replace(':', '-')
        if not time:
            if len(numbers) >= 10 and numbers[0:4] >= '2016' and numbers[0:4] < '2025':
                time = '%s-%s-%s' % (numbers[0:4], numbers[4:6], numbers[6:8])
        new_filename = os.path.join(OUTPUT_DIR, time + ' ' + os.path.basename(filename))
        if ALWAYS or not os.path.isfile(new_filename):
            out = add_label(img, time)
            print(filename, time)
            out.save(new_filename)
            return new_filename
    
    def add_label(img, caption):
        draw = ImageDraw.Draw(img)
        w, h = img.size
        border = int(min(w, h) * 0.02)
        font_size = int(min(w, h) * 0.04)
        # print(w, h, font_size)
        font = ImageFont.truetype(font_fname, font_size)
        _, _, text_w, text_h = draw.textbbox((0, 0), caption, font)
        overlay = Image.new('RGBA', (w, h))
        draw = ImageDraw.Draw(overlay)
        draw.rectangle([(border, h - text_h - 2 * border),
                        (text_w + 3 * border, h - border)],
                       fill=(255, 255, 255, 128))
        draw.text((border * 2, h - text_h - 2 * border), caption, (0, 0, 0), font=font)
        out = Image.alpha_composite(img.convert('RGBA'), overlay).convert('RGB')
        return out
    
    if len(sys.argv) >= 2:
        for a in sys.argv[2:]:
            if ALWAYS or not os.path.exists(a):
                print(a)
                try:
                    label_image(a)
                except Exception as e:
                    print("Error", a, e)
    

    I hope it all works out, since I've just ordered 120 4x6 prints covering the past three years or so…

    ]]>
    Adding a custom header argument to Org Mode source blocks and using that argument during export https://sachachua.com/blog/2023/01/adding-a-custom-header-argument-to-org-mode-source-blocks-and-using-that-argument-during-export/ Fri, 27 Jan 2023 15:11:01 GMT org emacs https://sachachua.com/blog/2023/01/adding-a-custom-header-argument-to-org-mode-source-blocks-and-using-that-argument-during-export/ I sometimes want to put long source blocks in a <details><summary>...</summary>...</details> block when I export to HTML, so that they're tucked away in a collapsible block. I tried using https://github.com/alhassy/org-special-block-extras to define my own #+begin_my_details "summary text" ... #+end_my_details block, but source blocks inside my_details doesn't get fontlocked properly while in the Org file. I wanted to add a :summary attribute to the regular src blocks, and to change the HTML export to wrap the code in details if the summary was specified.

    Code for adding a :summary argument and using it during export
    (setq org-babel-exp-code-template "#+begin_src %lang%switches%flags :summary %summary\n%body\n#+end_src")
    (defun my-org-html-src-block (src-block _contents info)
      (let* ((result (org-html-src-block src-block _contents info))
             (block-info
              (org-with-point-at (org-element-property :begin src-block)
                (org-babel-get-src-block-info)))         
             (summary (assoc-default :summary (elt block-info 2))))
        (if (member summary '("%summary" ""))
            result
          (format "<details><summary>%s</summary>%s</details>"
                  summary
                  result))))
    (with-eval-after-load 'ox-html
      (map-put! 
       (org-export-backend-transcoders (org-export-get-backend 'html))
       'src-block 'my-org-html-src-block))
    

    So now I can use it by specifying blocks like this:

    #+begin_src emacs-lisp :summary "Code for adding a :summary argument and using it during export"
    ;; code goes here
    #+end_src
    

    It took me a bit of digging around to figure this out. When I added the :summary attribute, org-babel-get-src-block-info found it when I was in the Org file, but by the time my-org-html-src-block was called, the block had been replaced with a copy that didn't have the header argument. I dug around using edebug's d command for displaying the backtrace, stepping through various functions. I found out that in the process for exporting source code blocks, org-babel-exp-code replaces the source block with the value of org-babel-exp-code-template, substituting certain values. Adding the summary flag to that and retrieving the summary information using org-babel-get-src-block-info worked. I originally used advice-add to override org-html-src-block, but I think I'll try replacing the transcoder.

    Adding custom header arguments could be useful for different export-related tweaks (someone wanted to create an argument for highlighting certain lines but hadn't figured it out in that thread). If there's a more elegant way to do this, I'd love to find out!

    This is part of my Emacs configuration.
    ]]>
    Making highlight-sexp follow modus-themes-toggle https://sachachua.com/blog/2023/01/making-highlight-sexp-follow-modus-themes-toggle/ Thu, 26 Jan 2023 15:25:38 GMT elisp emacs https://sachachua.com/blog/2023/01/making-highlight-sexp-follow-modus-themes-toggle/

    [2023-01-27 Fri] Prot just added a modus-themes-get-color-value function. Yay! Also, it turns out that I need to update the overlay in all the buffers.

    I'm experimenting with using the highlight-sexp minor mode to highlight my current s-expression, since I sometimes get confused about what I'm modifying with smartparens. The highlight-sexp background colour is hardcoded in the variable hl-sexp-background-color, and will probably look terrible if you use a light background. I wanted it to adapt when I use modus-themes-toggle. Here's how that works:

    (use-package highlight-sexp
      :quelpa
      (highlight-sexp :repo "daimrod/highlight-sexp" :fetcher github :version original)
      :hook
      (emacs-lisp-mode . highlight-sexp-mode)
      :config
      (defun my-hl-sexp-update-overlay ()
        (when (overlayp hl-sexp-overlay)
          (overlay-put
           hl-sexp-overlay
           'face
           `(:background        
             ,(if (fboundp 'modus-themes-get-color-value)
                  (modus-themes-get-color-value 'bg-inactive)
                (car
                 (assoc-default
                  'bg-inactive
                  (modus-themes--current-theme-palette))))))))
      (defun my-hl-sexp-update-all-overlays ()
        (dolist (buf (buffer-list))
          (with-current-buffer buf
            (when highlight-sexp-mode
              (my-hl-sexp-update-overlay)))))
      (advice-add 'hl-sexp-create-overlay :after 'my-hl-sexp-update-overlay)
      (advice-add 'modus-themes-toggle :after 'my-hl-sexp-update-all-overlays))
    

    This is what it looks like:

    highlight-sexp.gif
    Figure 1: Animation of highlight-sexp toggling along with modus-themes-toggle
    This is part of my Emacs configuration.
    ]]>
    Revisiting stenography and Twiddling https://sachachua.com/blog/2023/01/revisiting-stenography-and-twiddling/ Tue, 24 Jan 2023 15:02:06 GMT learning steno twiddler geek https://sachachua.com/blog/2023/01/revisiting-stenography-and-twiddling/ I've been thinking about what I can learn alongside A+ - something that will be slow to learn but that might be fun to get the hang of. I think it will help me practise patience and develop my empathy with her as she learns fine motor skills too. Maybe I'll even be able to model persistence and self-acceptance.

    At home, I practise steno on my Georgi keyboard while she does her homework, learns how to type, or writes stories. It works out pretty well, since I have to sound out words to chord them and I fingerspell slower than she types. I steno the words she wants me to spell for her, and I also steno the instructions on her homework. I steno my journal entries, too. I'd like to someday be able to write blog posts with steno. Not that I'm speed-limited now, but I'm curious about it and it's good to show A+ that I'm learning too. I made a webpage that lets me steno large text into a small textarea on my Android phone using Dotterel, displaying my cheat sheet and steno hints for the last few words using the main typeytype dictionary. That way, I can fingerspell words for A+ and then practise them as she copies the words. I'm also slowly going through The Art of Chording.

    I've also dusted off my Twiddler 2 one-handed chording keyboard, since that's something I can do while looking elsewhere. Looking outside makes me feel a little happier in winter. Maybe I'll even figure out how to write while waiting for her outside, perhaps bundling up my hand in a small blanket to keep warm. I mostly remembered how to chord with my right hand using the default layout, but I wanted to experiment with alternative layouts. I started learning a modified Backspice layout, moving some letters around since I can't easily reach 000L with my short pinky. I couldn't download the Twiddler 2.1 configuration tool from the Tekgear website, so I just programmed it interactively.

    To practice on the go, I set up Emacspeak in a Debian instance on UserLAnd on my Android phone, with audio output routed using pulseaudio to XServer XSDL via export PULSE_SERVER=tcp:127.0.0.1:4713 in my UserLAnd ~/.profile. It worked surprisingly well. I could press chords and hear what letter I typed. When I pressed SPC, I heard the word read out. This was enough for me to be able to explore the layout and think of words I want to spell with the letters I've found so far. I've been having a hard time figuring out how to easily get files in and out of UserLAnd aside from scp, though, as the document provider doesn't seem to show up for me.

    So I wrote a web-based tool that uses the Javascript Web Speech API to speak each letter as I type it and speak out the word after I press space–basically, the main things that I'd been using Emacspeak for. I also added a little cheat sheet that I could update on the fly, and I can have it read aloud by typing hlp and pressing SPC. I like this more self-directed, exploratory approach to learning the keymap. I press a chord and hear what letter it is, then think of words I want to spell with it and where those letters are. Here it is: twiddler.html (might not work on all browsers - I use it on Chrome on Android)

    Based on conversations on Mastodon, I decided to get the Twiddler 3 Wrap + Bluetooth. I wonder if the Twiddler 3 will make it easier for me to reach the far button with my pinky finger, and I'm curious if Bluetooth can still get through however many layers I want to have so that I don't get too cold. I probably won't use the Twiddler to write stuff while ostensibly standing around with the other grown-ups at a playdate. It's good for kids to see grown-ups being friends. But if there's standing-around time while she's off being independent, or if I want to look outside, it might be interesting to use.

    A+ sometimes gets frustrated with how slow writing is, or how she gets tripped up by a 3x3 perm she wants to learn. I'm glad I can slow down and learn something along with her.

    ]]>
    2023-01-23 Emacs news https://sachachua.com/blog/2023/01/2023-01-23-emacs-news/ Mon, 23 Jan 2023 14:41:57 GMT emacs emacs-news https://sachachua.com/blog/2023/01/2023-01-23-emacs-news/ [2023-01-23 Mon]: Added emacs.si meetup

    Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. 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!

    ]]>
    2023-01-16 Emacs news https://sachachua.com/blog/2023/01/2023-01-16-emacs-news/ Mon, 16 Jan 2023 14:23:06 GMT emacs emacs-news https://sachachua.com/blog/2023/01/2023-01-16-emacs-news/
  • Upcoming events: (could find another way to list the Mastering Emacs book club meetings; suggestions?)
  • Beginner:
  • Emacs configuration:
  • Emacs Lisp:
  • Appearance:
  • Navigation:
  • Navigation:
  • Dired:
  • Org Mode:
  • Denote:
  • Completion:
  • Coding:
  • Shells:
  • Web:
  • Mail, news, and chat:
  • Doom Emacs:
  • Multimedia:
  • Fun:
  • Community:
  • Other:
  • Emacs development:
  • New packages:
    • orgtbl-fit: Regression-fit a column in an Org Mode table (MELPA)
    • auth-source-gopass: Gopass integration for auth-source (MELPA)
    • xeft: Deft feat. Xapian (GNU ELPA)
    • gpr-query: Minor mode for navigating sources using gpr_query (GNU ELPA)
    • gnat-compiler: Support for running GNAT tools (GNU ELPA)
  • Links from reddit.com/r/emacs, r/orgmode, r/spacemacs, r/planetemacs, Hacker News, lobste.rs, planet.emacslife.com, YouTube, the Emacs NEWS file, Emacs Calendar, emacs-devel, and lemmy/c/emacs. 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!

    ]]>