How to Add Backlinks in Nanoc Static Site Generator

Since I’ve added backlinks to the bottom of posts today, I figured I might as well share the code.

I’m using static site generator nanoc. It has a preprocessing section in the compilation step where you can add new items and modify items that are read from disk further. I got the basis for this code from Denis Defreyne, creator of nanoc. (Check out his code.)

Denis’s code is actually very well suited to publish a set of notes with [[wiki link]] style links. If you want to create a static site wiki, definitely check out his approach.

This is a regular blog, though, so I modified the code a bit to match absolute URLs and relative URLs that involve the /posts path component. It also deals with the fact that some source files are called, and others are title-of-the-post/ I used to put them into folders to group them with images. But years later I wasn’t happy with that convention because now all files were called “”. So nowadays I’m mixing both and only put images into a subdirectory of the same name as the post.

A lot of tweaks later, the code is surprisingly long, but works pretty well:

preprocess do
  def find_backlinks
    backlinks_to = {}
    skipped_notes = ["/posts/overview"]
    domain = config[:base_url]
    # Start with the domain, or be inside a (/...) Markdown inline link or a [...]: reference style link
    link_regex = /(?<=#{domain}\/|\(\/|\: \/)(.*?)(?:="|\)|\s|$)/

    def remove_trailing_slash(str)
      if str[-1..] == "/"

    def trim_index(str)
      if i = (str =~ %r{/index.*})

    @items.find_all('/posts/**/*').each do |origin|
      next if origin.binary?
      next if skipped_notes.include?(origin.identifier.without_exts)
      # transform links into a format that looks like identifier.without_ext
      linked_paths = origin.raw_content
                       .map { |matches| matches[0] }
                       .map { "/" + remove_trailing_slash(_1) }
                       .map { trim_index(_1) }
      linked_paths.each do |linked_path|
        backlinks_to[linked_path] ||= []
        backlinks_to[linked_path] << origin.identifier

    @items.find_all('/posts/**/*').each do |target|
      next if target.binary?
      key = trim_index(target.identifier.without_exts)
      next if skipped_notes.include?(key)
      target[:backlinks] = backlinks_to.fetch(key, [])

  # call this before adding e.g. the tag index to limit the amount of @items

Then to render backlinks below the post, this is part of the layout template:

<% if @item[:backlinks].any? %>
  <aside id="backlinks">
    <h2>Links to this article</h2>
    <ul class="entries">
      <% @item[:backlinks].map { |id| @items[id] }.sort_by { _1[:title] }.each do |note| %>
        <li class="entry">
          <h3 class="title"><%= link_to(note[:title], note) %></h3>
          <%= Post::excerpt_for(note, no_readmore: true) %>
    <% end %>
<% end %>

The Post::excerpt_for function is an old helper I wrote in 2013 or so. It takes the first whole paragraph of a post instead of X characters, cutting off mid-sentence. The no_readmore option turns of adding “Continue reading…” links below the excerpt text. You can use the built-in #excerptize text helper for that instead.

Backlinks Added to the Blog

Everytime I mention, think of, and link to Andy Matuschak’s public notes, I really like how each note displays a list of backlinks at the bottom.

In my note-taking, I don’t want backlinks to be added automatically into the content. I can get by with other means just fine to figure out what links to the current note.

But for a hypertextual publication, adding a list of backlinks at the bottom is a neat tool to explore more, to find the beginning of an article series, or related items in general.

So I added a little section to posts on this blog. Not every page, just the posts.

Example: Check out a post with a couple of backlinks, including this post: How Do You Activate Sparkle’s XPC Services?

I’ll keep this around for a while and see how it affects finding the stuff I’m usually looking for on my own blog. More cross-connections make discovery in a publication easier, I’d argue.

Emacs Org-Mode: Automatic Item TODO/DOING/DONE State Transitions for Checkbox Changes

In Emacs org-mode, you start with two states for your outline headings by default to manage tasks: TODO and DONE.

I recently introduced a new state in between: DOING. That helped me come back to stuff I had to let lie for a while.

In code, that means at least:

(setq org-todo-keywords
      (quote ((sequence "TODO(t)" "DOING(g)" "|" "DONE(d)"))))

I actually have multiple sequences, but these don’t matter for this demonstrations.

Thanks to StackExchange, I had automatic parent-child-updates and “statistics cookie” actions for a while.

Video Demo

Check out a video demo of the TODO/DOING/DONE state transitions [on YouTube.](]

Cookies in Org-Mode

The “cookies” part is a summary of sub-items: of N TODO/DONE items, how many are done? That’s displayed on the parent with a cookie

It works both for checkbox list items and for nested outlines. To illustrate this, let me quote another, very short StackExchange answer:

In this org file:

* TODO Organize party [2/4]
  - [-] call people [33%]
    - [ ] Peter
    - [X] Sarah
    - [ ] Sam
  - [X] order food
  - [ ] think about what music to play
  - [X] talk to the neighbors

The [2/4] and [33%] are statistics cookies. They describe the completion status of the children of the thing they’re on.

They’re pretty useful, because they update automatically as you update the status of children. You can also use them to show the status of child TODO tasks:

* TODO bake a cake [3/6]
** DONE buy ingredients
** DONE start cooking
** DONE realize you forgot eggs, dammit
** TODO drive back to the store and buy eggs
** TODO wait, I needed THREE sticks of butter?
** TODO drive back to the store and just buy a damn cake

The [2/4] and 33% and [3/6] parts are all cookies. See the docs for a longer explanation.

Of course I don’t want to update them manually! org-mode does that for me, and there’s C-c # (invoking org-update-statistics-cookies) just in case. Cookie updates happen as I complete items, both in checkbox lists and TODO items.

Automatic Parent-Child State Updates

Very closely tied to this is an addition in my code that, when the cookie is updated, automatically completes the parent item.

* TODO complete these! [2/3]
- [X] first step
- [X] second step
- [ ] last step

Once I hit C-c C-c with the cursor in the last line to tick off the checkbox, the cookie is updated to [3/3] and the whole item changes to this state:

* DONE complete these! [3/3]
- [X] first step
- [X] second step
- [X] last step

It transitions from TODO to DONE automatically.

When I untick a checkbox, it switches back to TODO again, too.

This works the same when you replace checkboxes with sub-items where some are DONE and some are still TODO. The cookie policies shown above apply.

Introducing an Intermediate State to the Auto-Update

With the new DOING state, I needed the cookie update code to change a bit, because when I set an item to DOING and ticked off a checkbox, my code would see that it wasn’t yet finished and needed to transition to TODO. The whole point of the DOING state is to mark an item as work-in-progress and then keep it in that state.

So I changed the policy to take all three states into account. The implementation for sub-items with their own DONE states is simpler, so I’ll show it first:

(defun ct/org-summary-todo-cookie (n-done n-not-done)
  "Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
  (let (org-log-done org-log-states)   ; turn off logging
    (org-todo (cond ((= n-done 0)
                    ((= n-not-done 0)
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)

It simple because the hook already reports how many sub-items are done, and how many aren’t. (For checkboxes, I am going to need to parse the [x/y] cookie value.)

  • org-log-done and org-log-states are declared as local variables, overriding the global settings, and thus effectively turning off org-mode’s logging. I don’t use these and found it worked well to disable it here.
  • org-todo is a function that takes a new state and in this case updates the parent item. You don’t need to tell the function which item to update. The correct item is being activated, i.e. point moved there if needed, when the hook is called.
  • The trigger is in org-after-todo-statistics-hook, when the cookie is updated.

The three conditions are:

  • When the n-done count of DONE sub-items is 0 after the last change, i.e. when I uncomplete the last completed sub-item, it may be not a work-in-progress anymore. I change the state to TODO then. This doesn’t activate when there are no sub-items at all, because I need to toggle a sub-items state programmatically to make the hook execute.
  • When the n-not-done count of sub-items with other states, like TODO or DOING, is 0, that means we’ve completed everything. This might be a bit roundabout because of the negation you have to do in your head: When there are no not-done items, all items are done. There’s no other way to express “are all items completed” available here. (If we had n-total and n-done, we could test (= n-total n-done).)
  • The fallback/else clause: When 1 or more, but not all items are complete, then it’s a work in progress, so apply the DOING state.

How to Approach Auto-Updating the State (TODO/DOING/DONE) Based on Checkboxed

I already mentioned in passing that it’s a bit more work we have to do to achieve the same for checkboxes, because the built-in hooks don’t provide the same convenience. We don’t get a n-done count for a “checkbox changed” hook.

Instead, we have to rely on the “cookie updated” hook. This requires use of a cookie in the first place. The sub-item approach above works with and without cookies.

I’ll show the complete code for everything below, but here’s the approach I stole from the aforementioned StackExchange post years ago and adapted to my 3-state requirements:

  • Find the affected item’s line;
  • Use regular expressions to extract the [x%] or [n/m] cookies from the heading line;
  • Handle both percent and fractional cookies separately and update the state via org-todo like above.

The regex handling and the two cookie variants make the code a bit longer. Please see below for the implementation.

Complete Code

If you paste this into your init.el, you’ll get my whole implementation:

  • Change item state to TODO if no sub-items are DONE, or if the cookie reports [0/m] or [0%] completion.
  • Change item state to DOING when one but not all sub-items are DONE, or when the cookie contains a value above 0% and below 100% (aka for [n/m] where n < m and n > 0).
  • Change item state to DONE when all sub-items are DONE, or if the cookie reports [100%] or [m/m].

Please note that none of the hooks this relies on are called if you type the changes. If you type D-O-N-E for DONE, none of the org-mode facilities will note the state change.

You need to go through the interactive org-todo state change function (C-c C-t) or the Shift+Arrow_keys based state cycling to trigger hooks on the parent item in the outline.

For checkboxes, you need to tick them off with C-c C-c.

In case you forgot this and now nothing’s up-to-date anymore, you can trigger a cookie refresh by hitting C-c C-c with the cursor inside the cookie. So don’t be afraid.

(defun org-todo-if-needed (state)
  "Change header state to STATE unless the current item is in STATE already."
  (unless (string-equal (org-get-todo-state) state)
    (org-todo state)))

(defun ct/org-summary-todo-cookie (n-done n-not-done)
  "Switch header state to DONE when all subentries are DONE, to TODO when none are DONE, and to DOING otherwise"
  (let (org-log-done org-log-states)   ; turn off logging
    (org-todo-if-needed (cond ((= n-done 0)
                              ((= n-not-done 0)
(add-hook 'org-after-todo-statistics-hook #'ct/org-summary-todo-cookie)

(defun ct/org-summary-checkbox-cookie ()
  "Switch header state to DONE when all checkboxes are ticked, to TODO when none are ticked, and to DOING otherwise"
  (let (beg end)
    (unless (not (org-get-todo-state))
        (org-back-to-heading t)
        (setq beg (point))
        (setq end (point))
        (goto-char beg)
        ;; Regex group 1: %-based cookie
        ;; Regex group 2 and 3: x/y cookie
        (if (re-search-forward "\\[\\([0-9]*%\\)\\]\\|\\[\\([0-9]*\\)/\\([0-9]*\\)\\]"
                               end t)
            (if (match-end 1)
                ;; [xx%] cookie support
                (cond ((equal (match-string 1) "100%")
                       (org-todo-if-needed "DONE"))
                      ((equal (match-string 1) "0%")
                       (org-todo-if-needed "TODO"))
                       (org-todo-if-needed "DOING")))
              ;; [x/y] cookie support
              (if (> (match-end 2) (match-beginning 2)) ; = if not empty
                  (cond ((equal (match-string 2) (match-string 3))
                         (org-todo-if-needed "DONE"))
                        ((or (equal (string-trim (match-string 2)) "")
                             (equal (match-string 2) "0"))
                         (org-todo-if-needed "TODO"))
                         (org-todo-if-needed "DOING")))
                (org-todo-if-needed "DOING"))))))))
(add-hook 'org-checkbox-statistics-hook #'ct/org-summary-checkbox-cookie)

Possible Improvements

The hook for checkbox state updates is different. But to figure out if the item holding all checkboxes is complete, a cookie is required. Checkbox-ticking should also trigger the cookie statistics hook, though. So I think both implementations could be merged into one callback.

The basic code is 3 years old now. I guess org-mode v9.4 comes with new stuff that helps dealing with this, but which I haven’t discovered yet.

Are there any suggestions from your side, dear reader?

IPv6NAT in Unraid's Docker Containers Behind a Reverse Proxy to Make Outgoing IPv6 Requests Work

This tip is a bit out of context, but will make sense once I write more about the NAS I did set up.

The NAS I have runs Unraid. It’s a Slackware-based Linux operating system that provides a bit of redundancy/recovery from drive failures without the RAID setup overhead and performance penalties. More details will follow later.

With Unraid OS, you install web services as Docker containers. I have next to zero experience with Docker, so this was a “fun” experience.

For our ultimate goal to get rid of Dropbox, iCloud, Google Drive, and other cloud services, we wanted to have a self-hosted Nextcloud instance. In the past months, friends and I experimented with connecting our Nextcloud instances on our Raspberry Pis. That worked surprisingly well thanks to the power of IPv6.

A bit of backstore about IPv6: In Germany, it seems the trend is to vend IPv6 addresses from your internet service provider’s router to the computers in the network. Folks in chats from the UK told me they have a hard time getting an IPv6 at all. Here, you also still have IPv4 LAN addresses, like the classic namespace. Provided the router forwards ports 80 and 443 for HTTP and HTTPS traffic, respectively, I am able to use the IPv6 addresses of a friend’s device to get there from one machine in my network directly. I also just recently discovered this detail about IPv6 – that each IPv6 address in unique, unlike e.g., which is the LAN IPv4 address of my Unitymedia/Vodafone ConnectBox router, and all similar routers in all my neighbors’s homes. The router has a globally unique IPv6 namespace within which it can assign IPs freely. These globally unique namespaces are super cool, because you don’t need to use port forwarding in the router to forward to a particular device in the LAN. You just “open” the port and then then each device in your LAN with a globally unique IPv6 can be accessed from anywhere else on the planet. (Which is also a risk, of course.)

With the successful connection between Raspberry Pis, I wanted to get something similar with the Unraid NAS. It works in a similar fashion: install the Nextcloud Docker container, map its ports 80/443 to something that’s not used by Unraid itself (e.g. 8080/8443) and then you can access it directly using these ports.

But the approach I picked is installing a reverse proxy that receives all traffic on the standard ports and then forwards the requests to Docker containers based on the request, e.g. based on subdomain matching rules or sub-directories.

The subdomain-based approach works out this way:

  • is answered by the reverse proxy itself,
  • is received by the proxy, and based on a nextcloud.* rule, it forwards requests to a Nextcloud container.
  • is received by the proxy and forwarded to a phpmyadmin.* container.

Up until today, I had trouble with making outgoing IPv6 connections from containers in the proxy network, though.

The problem seems to be Docker’s own support of IPv6, especially with v19.10.3 that’s shipped with Unraid OS at the moment. (v20.x ship with IPv6 NAT, it seems!) IPv6 is an afterthought in Docker – at least that’s what folks on the internet say.

A simple test is to run ping6 – that works fine from the Unraid box itself, but not from within containers inside the reverse proxy network. Similarly, curl -vI <raspberry-pi-with-ipv6-address-only> times out. From within the Nextcloud container, sharing folders with the existing Raspberry Pi instances times out, too.

Solving IPv6 NAT with a Container

To make IPv6 work with Docker containers, we’re supposed to turn on IPv6 NAT. It’s a built-in feature since 20.03 or so, but not available in the Unraid Docker version.

With reverse proxies, the containers are expected to be in an internal bridge network, the proxy network, created via e.g. docker network create --ipv6 proxynet. Details will follow in later installments/write-ups. <!–ct: link to proxy network setup when published–>

To get this feature in the Unraid proxy net, install from Docker Hub in Unraid by adding a custom container:

  • Repository: robbertkl/ipv6nat
  • Network Type: Host
  • Privileged: On
  • Add path assignment: /var/run/docker.sock <-> /var/run/docker.sock (read-only)

Without the path, the log will show that unix:///var/run/docker.sock requests fail because the path isn’t available inside the container.

Assuming that the proxynet network exists, all containers except ipv6nat need to stay in the bridge proxy network. ipv6nat is part of the host network, so it can manage the other containers’s network settings/IP assignments.

After restarting the proxy and Nextcloud and what have you, outgoing IPv6 requests now should work!

I cannot yet interpret these findings. Did the IPv6 requests not go out at all? I don’t think so, because domains were resolved to IPV6 addresses; did the responses not reach the container? That’s likely.

So this is a fix to a problem that bugged me on and off for the past months – because, for one, the NAS itself did cost money to build but didn’t provide any value unless I could trust the whole thing to work properly.

With IPv6 NAT bolted on, Unraid 6.8 with Docker 19.10 can do the job. I bet in next to no time at all, Unraid 6.9 will come out and ship with a Docker version that does IPv6 NAT natively …

In the meantime, I’ll whip up an easier to install Unraid container for this purpose so others can benefit from this finding, too.

When Code Signing of Frameworks Fails During macOS App Distribution, Make Sure ENABLE_BITCODE=NO

Back in December, I whipped up a test project to demonstrate why distributing a Mac app with RxSwift using Carthage failed. Building and running worked well, but I couldn’t upload the app for notarization at all. The signing step just failed.

It told me to check the distribution logs, and in IDEDistributionPipelines.log, it read:

2020-12-07 15:10:48 +0000  Skipping architecture thinning for item "RxTestApp" because arch "arm64e" wasn't found
2020-12-07 15:10:48 +0000  Processing step: IDEDistributionODRStep
2020-12-07 15:10:48 +0000  Processing step: IDEDistributionStripXattrsStep
2020-12-07 15:10:48 +0000  Skipping stripping extended attributes of item: <IDEDistributionItem: 0x7fd1b3698110; bundleID='io.rx.RxCocoa', path='<DVTFilePath:0x7fd24432b9e0:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', codeSigningInfo='<_DVTCodeSigningInformation_Path: 0x7fd2200de2b0; isSigned='1', isAdHocSigned='0', signingCertificate='<DVTSigningCertificate: 0x7fd2266baa50; name='Apple Development: Christian Tietze (933RH59P6T)', hash='893EB53E4123EE2FBF9D7C6ED10005F804F2768E', serialNumber='<DVTSigningCertificateSerialNumber: 0x7fd186ffaf50>', certificateKinds='(
), issueDate='2020-09-21 08:10:09 +0000''>', entitlements='(null)', teamID='FRMDA3XRGC', identifier='io.rx.RxCocoa', executablePath='<DVTFilePath:0x7fd2370b1710:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', hardenedRuntime='1'>'>
2020-12-07 15:10:48 +0000  Skipping stripping extended attributes of item: <IDEDistributionItem: 0x7fd200ae2510; bundleID='io.rx.RxSwift', path='<DVTFilePath:0x7fd24433a010:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', codeSigningInfo='<_DVTCodeSigningInformation_Path: 0x7fd203c7cd00; isSigned='1', isAdHocSigned='0', signingCertificate='<DVTSigningCertificate: 0x7fd2266baa50; name='Apple Development: Christian Tietze (933RH59P6T)', hash='893EB53E4123EE2FBF9D7C6ED10005F804F2768E', serialNumber='<DVTSigningCertificateSerialNumber: 0x7fd186f7fca0>', certificateKinds='(
), issueDate='2020-09-21 08:10:09 +0000''>', entitlements='(null)', teamID='FRMDA3XRGC', identifier='io.rx.RxSwift', executablePath='<DVTFilePath:0x7fd200cd7140:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', hardenedRuntime='1'>'>
2020-12-07 15:10:48 +0000  Running /usr/bin/xattr '-crs' '/var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/XcodeDistPipeline.~~~30iivY/Root/Applications/'
2020-12-07 15:10:49 +0000  /usr/bin/xattr exited with 0
2020-12-07 15:10:49 +0000  Processing step: IDEDistributionCodesignStep
2020-12-07 15:10:49 +0000  Entitlements for <IDEDistributionItem: 0x7fd1b3698110; bundleID='io.rx.RxCocoa', path='<DVTFilePath:0x7fd24432b9e0:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', codeSigningInfo='<_DVTCodeSigningInformation_Path: 0x7fd2200de2b0; isSigned='1', isAdHocSigned='0', signingCertificate='<DVTSigningCertificate: 0x7fd221196d50; name='Apple Development: Christian Tietze (933RH59P6T)', hash='893EB53E4123EE2FBF9D7C6ED10005F804F2768E', serialNumber='<DVTSigningCertificateSerialNumber: 0x7fd221456060>', certificateKinds='(
), issueDate='2020-09-21 08:10:09 +0000''>', entitlements='(null)', teamID='FRMDA3XRGC', identifier='io.rx.RxCocoa', executablePath='<DVTFilePath:0x7fd2370b1710:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', hardenedRuntime='1'>'>: {
2020-12-07 15:10:49 +0000  Writing entitlements for <IDEDistributionItem: 0x7fd1b3698110; bundleID='io.rx.RxCocoa', path='<DVTFilePath:0x7fd24432b9e0:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', codeSigningInfo='<_DVTCodeSigningInformation_Path: 0x7fd2200de2b0; isSigned='1', isAdHocSigned='0', signingCertificate='<DVTSigningCertificate: 0x7fd2224a0da0; name='Apple Development: Christian Tietze (933RH59P6T)', hash='893EB53E4123EE2FBF9D7C6ED10005F804F2768E', serialNumber='<DVTSigningCertificateSerialNumber: 0x7fd1a668b350>', certificateKinds='(
), issueDate='2020-09-21 08:10:09 +0000''>', entitlements='(null)', teamID='FRMDA3XRGC', identifier='io.rx.RxCocoa', executablePath='<DVTFilePath:0x7fd2370b1710:'/Users/ctm/Library/Developer/Xcode/Archives/2020-12-07/RxTestApp 07.12.20, 16.10.xcarchive/Products/Applications/'>', hardenedRuntime='1'>'> to: /var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/XcodeDistPipeline.~~~30iivY/entitlements~~~AkwhDT
2020-12-07 15:10:49 +0000  Running /usr/bin/codesign '-vvv' '--force' '--sign' 'F1C8B9C025AADFD0F5A1C94704B713AE48E05B2D' '--entitlements' '/var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/XcodeDistPipeline.~~~30iivY/entitlements~~~AkwhDT' '--preserve-metadata=identifier,flags,runtime' '--requirements' '=designated => anchor apple generic  and identifier "$self.identifier" and ((cert leaf[field.1.2.840.113635.] exists) or ( certificate 1[field.1.2.840.113635.] exists and certificate leaf[field.1.2.840.113635.] exists  and certificate leaf[subject.OU] = "FRMDA3XRGC" ))' '/var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/XcodeDistPipeline.~~~30iivY/Root/Applications/'
2020-12-07 15:10:49 +0000  /var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/XcodeDistPipeline.~~~30iivY/Root/Applications/ replacing existing signature
2020-12-07 15:10:49 +0000  /var/folders/62/8k21681d08z9lhq8h433z3rh0000gp/T/XcodeDistPipeline.~~~30iivY/Root/Applications/ code object is not signed at all
2020-12-07 15:10:49 +0000  /usr/bin/codesign exited with 1

The important line:

/Frameworks/RxCocoa.framework/Versions/A: code object is not signed at all

How did that happen?

I’m on macOS Catalina and the more recent Xcode 12 toolchain includes arm64 builds for M1 Macs.

Thanks to Matteo Rattotti of for pointing out that this is a problem of the Rx project setup: ENABLE_BITCODE must be set to NO. Turns out he had to sift through their dependencies for quite a while to figure that out. Thanks for saving me hours of guess-work (on top of all the hours I was dumbfoundedly looking for a cause of this failure).

By now, most of your dependencies may already have ENABLE_BITCODE=NO set, but if that’s not the case, or if you cannot upgrade to more recent versions, you can override the setting in a fork of using your dependency manager of choice.

You can enforce this in Podfile overrides of project settings more or less like so:

post_install do |installer|
  installer.generated_projects.each do |project|
    project.targets.each do |target|
      target.build_configurations.each do |config|
        config.build_settings['ENABLE_BITCODE'] = 'NO'

For Carthage, you can override settings, too, but have to use a custom shell script to make this a bit simpler, like so:

#!/usr/bin/env bash

# Usage example: ./ build --platform iOS

set -euo pipefail

xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX)
trap 'rm -f "$xcconfig"' INT TERM HUP EXIT

# (other settings here)
echo 'ENABLE_BITCODE = NO' >> $xcconfig

export XCODE_XCCONFIG_FILE="$xcconfig"
carthage "$@"

I didn’t know you can override project settings for Carthage until building frameworks in Xcode 12 broke, but now I cannot imagine living without it – the utility of quickly trying to fix stuff by patch settings is immense.

SOLVED! Looking for Docker Reverse Proxy & IPv6 Setup Help (Paid)

Update 2021-02-16: I accidentally solved the problem myself, with help by Docker specialist @cwrau using a IPv6 NAT container

I need help with a NAS (running the Unraid operating system) and Docker containers (Docker v19.03.5 at the moment) for various services.

My setup includes

  • a nginx reverse proxy
  • a NextCloud container
  • a GitLab/Gitea container (and maybe others in the future once the problem is solved)
  • a manually created Docker network (“proxynet”)

All these services work alright for me. The issue I have is with outgoing IPv6 connections. I don’t have a stable IPv4 but I have a dynamic DNS pointing at my NAS’s IPv6 address. I can connect from computers and mobile devices to NextCloud just fine.

I cannot ping6 from within any Docker container in the proxy network, or the reverse proxy container itself, though. I also cannot connect to other NextCloud instances that are only available via IPv6, rendering the federation/sharing aspect moot.

I tried a couple of things, like running a ipv6nat container next to the others, but I don’t understand enough of the underpinnings and maybe Unraid’s limitations to make this work in reasonable time.

(Call to action removed since this is solved)

Throwing out the reverse proxy is an option, but I’d rather keep it in place and eventually use it to hide the rest of my network behind it as well. (I do have a Raspberry Pi or three in the LAN as well.)

Disable NSTextAttachment Action and Sharing Services Menu Drop-Down

By default, NSTextView will show the NSSharingServicePicker button when you hover over an image inside the text view. That’s true even for custom image-based NSTextAttachments in the attributed string.

The NSSharingServicePicker with some system services showing for an image in TextEdit

The default menu item is limited to “Markup” and a “Services” submenu, I believe. Apps can register to be shown in this menu, and users can customize the menu in System Preferences.

But what if you want to hide this dropdown button thingie completely?

I don’t know why the official means to react to the service picker don’t work, but here’s something that does the trick: override the default behavior in a NSTextView subclass.

class MyTextView: NSTextView {}

extension MyTextView: NSSharingServicePickerDelegate {
    public func sharingServicePicker(
        _ sharingServicePicker: NSSharingServicePicker,
        sharingServicesForItems items: [Any],
        proposedSharingServices proposedServices: [NSSharingService]
    ) -> [NSSharingService] {
        // Deactivate sharing services completely.
        return []

In my mind, this is only a temporary quick fix until I figure out why the default pathways don’t trigger as expected. Things to look into include the setup of the NSTextAttachments – do the default pathways work when you use a NSTextAttachment with a FileWrapper instead of a custom cell? Do they work if you declare rich text support on the text view?

If you know anything, I’d love to hear how you tackled this.

Create Custom Org-Mode Links to Open My External Zettelkasten App

I’m a fan of linking into my Zettelkasten. I usually do this via a convention: when a 12-number digit is used to signify a timestamp with accuracy to the minute, like 202102101025 for 2021-02-10 10:25, then I expect this to be a note identifier in my note archive. When the timestamp is accurate to the second, I expect this to be something else outside my note archive, like invoices I filed away. To utilize this information, in the worst case, I have to copy the ID and paste it into Spotlight to get to the note.

For my own note-taking app, The Archive, there is a URL scheme to remote-control the app the exact same way that internal links between notes work: thearchive://match/IDENTIFIER.

That can be used for rich text editors to create hyperlinks.

That can also be used to create outgoing links from my Emacs org-mode files.

For example I picked up a fix to a problem on the web, created a note so I can later remember what and why and how, and then I want to attach this note to a task in my org-mode project files. I can register a new link type, zettel, to make this work. The types are prefixed like URL schemes in org links, so I’d use it like so:

See: how to fix XYZ.[[zettel:202102101033]]

I could also add an optional link anchor text, but I usually don’t like to have a very long clickable link. I prefer links to not take up a whole line, or most of it, because the stark blue color is rather distracting from the text around it.

This is the Emacs lisp code to register the link prefix:,


  :follow (lambda (searchterm)
                  (browse-url (concat "thearchive://match/" searchterm)))

  ;; Don't fold links but show ID and description:
  ;;   [[zettel:202102101021][Title or description here]]”
  :display 'full)

With that in place, zettel:-prefixed links in org files execute the URL scheme for The Archive. You can replace the URL scheme with something else, if you don’t use my app.

There’s a ton of customization I just skipped. I got the inspiration for this while reading How do I make a new link type? in Musa Al-hassy’s article on org-special-block-extras – it is packed with a ton of customizations to transform org-mode blocks into interactive HTML pieces, argument trees, and whatnot. It’s crazy and I don’t understand half of it. Go read it.

A Short Note on Voice Assistants and Very Old People Who Don't Hear Well

In reaction to my post about the hyperlegible font, from earlier this week, people on HackerNews kindly pointed out that voice-assistants might be a fix. Thanks for all the ideas and comments, folks.

I didn’t discuss everything about the granny-situation in that post to keep it short, so y'all don’t know everything, of course. I’ll follow up on the topic some later day, because there’s still so much to be said, but here’s the advance summary of voice assist technology: it only works when you either know what you have to do, or when your perception is still good enough to train to use these reliably.

Chances are, if you’re very old, more than one things will give out with time. My grandmother can’t see well at all, but she can’t hear well for even longer. It’s really bad. It was way worse than my grandfather could hear when he was 90 and she was about 85. Bad luck, bad genes – it’s what it is, and you probably don’t know in advance how hard life will hit you. On the plus side, she’s still pretty sharp; on the negative, she’s bored to death because she can’t do anything at all very well to keep herself entertained. So she’s not like a 20-year-old competetive athlete who happens to have bad eyesight but otherwise perfectly fine body functions. Everything’s giving out on her at the same time.

For us young ones, it’s easy to joke about Siri misunderstanding what we said. (It appears to be worse in German, but there’s plenty of memes in English, too.) But if you depend on voice input for names, and audio feedback to notice that the assistant dialed the wrong person, but can’t hear the assistant very well, you’re screwed. My grandmother doesn’t say “Ah bugger off Siri” and tries again, or gets a laugh out of funny interpretations like the 7-year-olds in our family do. She’s utterly confused because she doesn’t know what happened, why the phone doesn’t seem to dial, or if it does in the first place because she can’t hear the doot … doot sound of the phone sometimes when it’s waiting for the other party to pick up. Imagine putting a phone to your ear, and wondering why nobody picks up, while to bystanders it’s obvious that, d'oh, you need to hit the green button to dial the number, first. Which she did try to press, but maybe her thumb hurt that day and she didn’t press hard enough, and the press didn’t register, or whatever.

To her, voice assistants sounds like someone’s talking with a potato in their mouth, while being stuffed in a bag and then hidden in the drawer. It’s garbled, hollow, and muffled at the same time.

I’m lucky to be able to talk with her, and to have a sufficiently deep voice that she can pick up, if I’m being loud enough, and pronounce words carefully, and choose words that sound distinct from each other. That’s a lot of ifs, a lot of things that can and do go wrong and I have to rephrase something so she can get what I want to bring across.

I believe her brain is picking up parts and recognizes pieces of familiar sounds. It then fills in the gaps. Like speed-reading, but without speed, and a higher error rate. I cannot, I just can not by sound alone tell her the name of the street I live in: “Elpke”. Never heard that, so she mis-understands it 100% of the time, even though the syllables don’t seem to be that weird. The p/k sound combo is odd, so I’d totally get if she thought I was living in “Elke”. But she ends up at “Telgte”, with the characteristic “ch” instead of “g”. That word she knows. Her pattern recignition machine still works. The inputs are just too bad.

Anyway, that’s also why my grandmother and her daughter can’t talk about much at all, because my aunt’s voice is too thin and high-pitched. On the upside, my grandmother knows me since forever, and my voice didn’t change much in the past 15 years or so I’d guess, so she had plenty of time to build up expectations of what talking-with-Christian sounds like. Even then, me being the Wizard of Oz-style voice assistant fails.

A robot voice just doesn’t cut it. A pleasant, soft-spoken voice won’t be picked up. You can’t easily make it repeat itself. You can’t make it patient, or come up with ideas to approach the situation from a different angle. Computer says no, and that’s the end of it. It resets its state at some point. My grandmother can’t. She’s changed by the interaction: from a state of wanting to do something to a state of not wanting to do anything at all, because she’s failing, and all the tools are failing her, and the world just got a bit more depressing because of all that.

That’s why voice assistants are not a solution, but huge-ass printouts are. Even illegible print-outs are a puzzle she can solve through magnification. The analogue world is one she still understands and can manipulate, to some extent, with her cyborg tools of electronic magniying glasses and hearing aids.

Atkinson Hyperlegible Font May Be Pretty Good If Your Granny Can't See Well

My grandmother is 91 years old and, for about 2 years now, her sight degraded to almost-blindness. She barely sees milky shapes in her central field of vision. It’s supposedly better in the corners of her eyes, but I couldn’t get any reliable confirmation out of her regarding that.

So using a telephone is a problem.

There’s a ton of phones for the hearing-impaired. And there’s phones for those with really bad sight. But I don’t really buy these claims from manufacturers. We’ve been through somewhere between 5 to 10 phones. I should probably list them here one day so you, dear reader, don’t have to try all the phones, either. It’s a road full of disappointments, I can tell you.

In the past, she memorized the number of presses on the “down” key that were required to scroll through her phone book to call someone. TAXI was at position 2, doctors at positions 3, 12, and 18 or so, I was at around 5. That worked remarkably well, all things considered. She couldn’t easily add entries to the phone book, though, because that threw the old order out of the window. And the display was garbage: black BG and white text at high resoltion sounded good, but font sizes were too small and selections were highlighted with a medium-red background and the same white font, effectively reducing contrast instead of increasing it (when you come from white-on-black, you can only get down from that 100% tonasl contrast). Using her b/w contrast loupe thingie and reading device, the result was a mushy gray. Could just as well not have a display at all.

Update 2021-02-05: Folks pointed out that I didn’t discuss voice assistants at all in this context, so I wrote a short follow-up why they are not an option if you’re curious.

My current quick fix for her needs: huge printouts. 72pt font, one line for the name, one line for the number.

With a quick mockup using my iPad and my father’s WiFi printer on premise (amazing how well that stuff worked together out of the box), the best thing I could whip up was Baskerville Bold, 72pt.

First page of the printout in Baskerville Bold 72pt on A4 paper. Numbers are all businesses listed publicly.

She approved of that design because it was rather big and rather black. It kinda worked for a test number or two.

Transcribing the whole phone book of hers, I looked for fonts expecially designed for people who have impaired sight. I know there’re fonts to aid dyslexic – where the line weights are somewhat unique and help differentiate the letters, though I don’t know how well these perform.

I did find the free Atkinson Hyperlegible Font by the Braille Institute of America, Inc., on the web, though. It looks unremarkable at first. I was worried because it’s a sans-serif, so I expected that it would perform worse than Baskerville becaus of the lack of shape hints in the serifs.

But I was wrong, she liked the font better, so we kept that.

Same page with the hyperlegible font at 75pt. I couldn’t fit 4 entries on a page, so I increased overall whitespace

One nice thing the font features out of the box is a slash in the number 0. Numbers in general are designed to not all look alike. We’ll see how well that works after a couple of weeks. Muscle memory to hit the numbers on the phone is okay, and having a printout that she can even further magnify if needed beats the crappy phone displays any day of the week.

I can recommend the font. If you have someone with impaired sight, try it out in your printouts and see how it performs. Atkinson Hyperlegible Font is a free download. Thanks, Braille Institute of America!

→ Blog Archive