Replacement for NSAppearance.performAs­Current­Drawing­Appearance on macOS 10.14 and 10.15 to Fetch the Correct NSColor.cgColor

Today, I had trouble getting NSColor to work with colors from Asset catalogues when asking for its .cgColor.

Since NSColor is appearance-aware, i.e. it switches light and dark mode appropriately when used directly in your views, I wondered why asking for .cgColor always returned the initial value. Say we start the app in light mode, then this is always going to be the light mode color, never dark mode. Yes, not even if you initialize the color anew using NSColor(named: ...).cgColor.

I knew that storing the CGColor won’t dynamically update the result, but not even the computed .cgColor property? – That implies it’s not NSColor’s job to be appearance-aware. It’s someone else’s job.

That didn’t let me leave work in peace, so I spent my evening fiddling with this for an hour or two with a test app. I found that NSAppearance.performAs­CurrentDrawing­Appearance { ... } does the job. But that’s only for macOS 11+, so it won’t do.

Searching the web for performAsCurrentDrawingAppearance produces very little results. Nobody else wanting this on macOS 10.14 and 10.15? The Mozilla bug tracker has a ticket that contains a fix that sets NSAppearance.current before performing some drawing stuff – that was the only hint I found in that web search that made things click. You can and should set NSAppearance.current just like you work with fill colors and NSGraphicsContext stuff. It’s some global state, but it’s also supposed to be changed by you all the time.

In hindsight, Daniel Jalkut told as much in his 2018 article on Dark Mode:

NSAppearance.current or +[NSAppearance currentAppearance] is a class property of NSAppearance that describes the appearance that is currently in effect for the running thread. Practically speaking you can think of this property as an ephemeral drawing variable akin to the current fill color or stroke color. Its value impacts the manner in which drawing that is happening right now should be handled. Don’t confuse it with high-level user-facing options about which mode is set for the application as a whole. [Bold emphasis mine.]

I just didn’t connect the dots properly.

And with these hints, I wrote a block-based helper that would replace performAs­CurrentDrawing­Appearance for me:

extension NSAppearanceCustomization {
    public func performWithEffectiveAppearanceAsDrawingAppearance<T>(
            _ block: () -> T) -> T {
        // Similar to `NSAppearance.performAsCurrentDrawingAppearance`, but
        // works below macOS 11 and assigns to `result` properly
        // (capturing `result` inside a block doesn't work the way we need).
        let result: T
        let old = NSAppearance.current
        NSAppearance.current = self.effectiveAppearance
        result = block()
        NSAppearance.current = old
        return result

extension NSColor {
    /// Uses the `NSApplication.effectiveAppearance`.
    /// If you need per-view accurate appearance, prefer this instead:
    ///     let cgColor = aView.performWithEffectiveAppearanceAsDrawingAppearance { aColor.cgColor }
    var effectiveCGColor: CGColor { NSApp.performWithEffectiveAppearanceAsDrawingAppearance { self.cgColor } }

This will now get the effective CGColor of any NSColor, based on the NSApp.effectiveAppearance.

For finer-grained control, create a context by using aView.performWith­EffectiveAppearance­AsDrawingAppearance { ... } instead. That allows each view to opt out of automatic appearance changes and set its context appropriately.

So today I learned that NSColor from Asset catalogues do not magically auto-resolve the required appearance; NSColor relies on NSAppearance.current to be set properly. That’s done by views (in draw(_:), for example, but not in viewDidChangeEffectiveAppearance() – I checked).

NSTextView Performance May Degrade for Large Plain Text Documents When usesFontPanel Is Active

I was profiling performance bottlenecks in the The Archive and noticed that no matter how much highlighting functionality I removed/commented-out, the apparent slowness was all due to … Touch Bar API?!

1.89 s   98.0%	0 s	   -[NSTextView(NSSharing) setSelectedRanges:affinity:stillSelecting:]
1.00 s   51.8%	0 s	    -[NSTextView updateFontPanel]
1.00 s   51.8%	0 s	     -[NSTextView(NSTextView_TouchBar_API) updateTextTouchBarItems]
1.00 s   51.8%	0 s	      -[NSTextTouchBarItemController setSelectedAttributesWithEnumrator:]

I have been “pruning” the Touch Bar related calls from the profiling stack to focus on what I though would be the real bottlenecks. But, as often, it turns out this was stupid and the instruments did point out the true problem. Something indeed was causing trouble here, it turned out.

Luckily, all my internal libraries/frameworks have little example apps included, and the innermost Markdown highlighting library is no different. The same file that was causing problems in The Archive was working fine there all the time, and that was my main motivator to comment-out stuff I added in modules that used the highlighter and focus on that. But in the end all that was left was a difference in the setup of Text Kit components, and there I found the real culprit. It is this setting that was activated for The Archive but not for the much faster sample app:

textView.usesFontPanel = true

That tells NSTextView to respond to font changes in the system standard font panel (often bound to ⌘T or some such in text editors).

Turning this off made the time-intensive Touch Bar API calls go away immediately. The call stack would’ve told me as much if only I hadn’t dismissed the information it was providing.

I’m not quite sure why this makes both clicking around in the text (i.e. changing the selected range) and typing so slow, yet.

I do know that some rich text editors (including TextEdit) show rich text control buttons in the Touch Bar, like buttons to control bold/italic/underline; and these would need to update as you type or move the insertion point. But these buttons aren’t even visible in a plain text NSTextView. The only text view related touch bar button is the Emoji picker. So I’m not sure why there’s this slow-down when the Touch Bar wouldn’t even need to update. All I can share at the moment is that disabling usesFontPanel outright eliminated this performance problem.

By the way, this problem was reported by a user running macOS High Sierra, so it’s not just a Big Sur bug. He also has no Touch Bar, so it might be possible that the performance bottleneck surfaces even if my dev machine was a Touch Bar-less Mac Mini or something. Not sure, though. Imagine how annoying it’d be to find this performance problem if I didn’t have a MacBook Pro with a Touch Bar, and you’d need a Touch Bar to make the problem surface. That’d be no fun at all.

Why did I use font panel support in a plain text Markdown app at all? To let users modify the app’s default font settings from the font panel, if they so desire, without going to the app preferences. Seems we can’t have nice things, though, so that might have to go away in the next update.

I’ll keep an eye open and investigate. But this is such a stupid thing that I wanted to share it as quickly as possible.

So if your syntax highlighting is slow for large documents, and your Time Profiling instrument points out something related to the Touch Bar API that somewhere in the call stack mentiones “font panel”, try usesFontPanel = false.

Retry Imperative Conditions with RxSwift Using a Delay

In The Archive, people relying on character composition to enter their text noticed that the auto-saving routing got in the way and aborted the composable editing mode.

This affects e.g. Chinese or Japanese character input on macOS, but also when you hit a composable accent like ´ after which the text editor waits for another character to put underneath the accent.

This composing editing mode is handled in NSTextInputClient via what they call “marked text”. The accent is not actually part of the NSTextView.string until you finish the composition. Same with e.g. Chinese input: you hit a bunch of keys and see composition interface on screen, but the actual string isn’t changed until you commit the change.

Since the underlying string isn’t changed, when users activate this composition mode, none of the key presses during that mode fire textDidChange or similar notifications.

When The Archive knows the user is idle, the app reacts to external file changes by displaying them right away. The idle check was bound to text changing notifications until now, so when you were using the keyboard layout for e.g. Chinese Pinyin Simplified and typed a bunch of keys, the composition mode would stay active for quite a while, not registering as “idle”.

This posed a problem in The Archive when this composition mode was active.

To check if that mode is active, a text view’s hasMarkedText().

The idle signal I am relying on lives in RxSwift land and worked like this: 1 second after the last user input or selection change, change the internal state to .idle.

To account for character composition to take longer, I had to prevent this .idle event from firing while hasMarkedText() returned true. While hasMarkedText() returns true, no other interaction with the text view is registered, so I couldn’t just ignore the .idle event – there wouldn’t be another one coming later – but had to delay it.

Why not just ignore the .idle event? After all, when users finish character composition, the text view content is changed as usual and 1 second later another .idle event would fire. But when users abort the composition, no such change event occurs. If I just drop the .idle event, then the user aborts character composition, the text view would just not begin to idle at all anymore.

Digging around in RxSwift extensions, Marin’s post about retry pointed me to RxSwiftExt’s implementation of retry from which I stole the implementation of a delay:

The use of Observable.just(()).delaySubscription(...) took some time to get used to. The immediate signal of () is just the hook to apply the actual delay. Delaying the subscription means whatever is actually happening when subscribing or flat-mapping to this observable sequence is delayed. We’re not subscribing to this sequence directly, so it affects the content of the .flatMapLatest block.

First, here’s how I use it:

let idlingAfterUserEdit = anyUserInteraction
    // Debouncing will wait for the duration to pass after
    // the latest edit, so we effectively have 1s idle delay.
    .debounce(.seconds(1), scheduler: MainScheduler.instance)
    // Delay event production while composition is active
    .flatMapLatest { _ in
        retry(until: { $0.hasMarkedText() == false },
              on: textView,
              delay: .seconds(2))
    // Handle retry timeout: just begin to idle then.
    .map { _ in .idle }

Now here’s the retryUntil implementation:

/// Produces a success event `()` either right away if `test` passes,
/// or after N tries (up to `maxTries`) with `delay` between each try.
/// - Returns: Observable sequence producing a `()` signal once the
///   condition is met, or an error when it times out.
func retry<T>(
    until test: @escaping (T) -> Bool,
    on object: T,
    delay: RxTimeInterval,
    scheduler: SchedulerType = MainScheduler.instance,
    maxTries: UInt = .max)
-> Observable<Void> {
    return retry(
        until: test,
        on: object,
        delay: delay,
        scheduler: scheduler,
        maxTries: maxTries,
        currentTry: 0)

private func retry<T>(
    until test: @escaping (T) -> Bool,
    on object: T,
    delay: RxTimeInterval,
    scheduler: SchedulerType = MainScheduler.instance,
    maxTries: UInt,
    currentTry: UInt)
-> Observable<Void> {
    if currentTry > maxTries {
        return .error("retryUntil: \(currentTry) over limit")
    if test(object) {
        return .just(())
    return .just(())
        .delaySubscription(delay, scheduler: scheduler)
        .flatMapLatest {
            retry(until: test,
                  on: object,
                  delay: delay,
                  scheduler: scheduler,
                  maxTries: maxTries,
                  currentTry: currentTry + 1)

I didn’t find value in taking the extra time to make this truly generalizable over any Observable<Element> sequence; I don’t actually care about the contents of the event that I want to delay since I’m mapping them to .idle anyway without even looking. But if you add that to your code base, please do share.

As always with RxSwift, I’m not feeling overly confident in the approach or in my ability to understand what’s going on, even if I extract well(?)-named helpers like this that don’t do much.

To inspect the call stack and see if letting this run UInt.max times would result in a stack overflow, I did what every good caveman does:

if currentTry > 10 { fatalError() }

Nope, looks good; the delay schedules the block itself on the target queue like DispatchQueue.main.asyncAfter(...) would.

The good news so far is that delaying the .idle event until after the comoposition mode has ended does wonders. Auto-saving of the contents still happens in the background, but the editor doesn’t abort composition and display external updates.

All of this, by the way, would’ve been so much simpler if the marked text API had some kind of notification or delegate callbacks. I pondered adding this myself, but the edge cases are too messy – e.g. you can’t rely on unmarkText() being called when the user aborts composition by clicking outside the app and bringing another window into focus.

FastSpring Introduces Multi-Discount Coupon Codes

Recently, FastSpring announced what they call “Multi-Discount Coupons”. These are coupon codes that:

  • can be used multiple times (e.g. CYBERMONDAY, to be used by any customer, as opposed to one-time use coupons)
  • can apply different discounts for multiple products.

This is different from regular coupon codes that would only apply to one product.

To implement a coupon-based discount for a combination of products, the best bet so far was to create a (temporary) product bundle and apply a re-usable coupon to that.

I’m glad to see FastSpring is still expanding the features of the new backend. It’s catching up to the decades old backend, and this is, I believe, even exceeding that one’s features.

In the old backend, you were able to do many more discount-based offerings that were commonplace. When the new backend was introduced, I lamented that the established patterns of offering discounts weren’t possible. We exclusively had one-time use coupons. So you couldn’t run a sale with a CYBERMONDAY coupon that anyone could use an infinite number of times (you had to generate unique codes like CYMON01A45). And you couldn’t give leads from a sponsored blog posting a discount as an incentive to purchase. (I am not implying that this is a good or bad idea. Just that it wasn’t possible, while being a very common practice; “Use coupon CTIETZE2021 for 20% off!”)

The best thing you had was to apply referral-based discounts, or, worse, create a discounted, hidden copy of your product and link folks to that. Meh.

So things are improving for sellers, that’s good.

How to Fix Mach-O Header Code 0x72613c21 When You Try to Export Your App in Xcode

I was preparing a test build to check if linking against a new library worked fine in production. Trying to distribute the app using my Developer ID (but this would also have happened in a step before uploading to the App Store), I got this:

Found an unexpected Mach-O header code: 0x72613c21


Like probably most developers, I have absolutely no clue about many crucial steps in the app making process. Especially stuff like code signing and most build settings elude me – I pick up pieces over the years, but not with the same speed with which I’m getting better at writing apps. So I didn’t understand what’s going on at all here. The search results I got weren’t that useful, so this post is meant to fill the vacuum for the next person running into this.

Didn't hurt to also make a video demo if you don't know where to click

Code 0x72613c212 Indicates You Are Embedding a Static Library

As far as I understand the message, embedding a static library in your app binary can produce code 0x72613c21. There may be other causes. But in short, this was the issue for me.

My app bundle is embedding a .framework bundle which in turn embeds a 3rd party library. The first embedding step is fine. The second isn’t, because the 3rd party library is a static library. I didn’t pay attention to this because I had no clue this is a problem in the first place.

An Apple Technical Note, TN2435, has an explanation for this. It’s a good resource, but it wasn’t among the first suggestions of my initial search; it did pop up when I asked the search engine why not embed static libraries xcode, though:

The following error indicates your app embedded a static library.

Found an unexpected Mach-O header code: 0x72613c21

This is caused by placing a static library in a bundle structure that looks like a framework; this packaging is sometimes referred to by third party framework developers as a static framework. Since the binary in these situations is a static library, apps cannot embed it in the app bundle.

How to Check If a Binary Is a Static Library

If you’re not sure if you have a static or dynamic library, TN2435 even includes instructions to check:

Terminal command to determine if a binary is a static library

file <PathToAppFramework>/<FrameworkName>.framework/<FrameworkName>

Example output for a static library

Mach-O universal binary with 2 architectures

<PathToLibrary> (for architecture armv7): current ar archive random library

<PathToLibrary> (for architecture arm64): current ar archive random library

I checked, and sure enough, this is the truncated output. Looks similar:

.../Versions/a/libMultiMarkdown: Mach-O universal binary with 2 architectures:
    [arm64:current ar archive random library] [x86_64:current ar archive random library]
.../Versions/A/libMultiMarkdown (for architecture arm64):
    current ar archive random library
.../Versions/A/libMultiMarkdown (for architecture x86_64):
    current ar archive random library

Compare this to a regular framework built in Swift:

.../Versions/A/JSONAttachment: Mach-O universal binary with 2 architectures:
    [x86_64:Mach-O 64-bit dynamically linked shared library x86_64]
    [arm64:Mach-O 64-bit dynamically linked shared library arm64]
.../Versions/A/JSONAttachment (for architecture x86_64):
    Mach-O 64-bit dynamically linked shared library x86_64
.../Versions/A/JSONAttachment (for architecture arm64):
    Mach-O 64-bit dynamically linked shared library arm64

There, you see “dynamically linked”, so conversely, the other isn’t.

This TN2435 I have just found when I wanted to look for more references for my own notes. I didn’t know about this yesterday when I ran into the problem. Most search results I found suggested using otool -h and otool -f instead. But I could not make sense of the results at all. And for the library in question, the static lib appeared to be the concatenated result of many .o files, so the output was crazy long, too.

Instead of even bothering with otool for library inspection, just check the binary product with file instead.

Fix: Do Not Embed Static Libraries

The solution, also mentioned in TN2435, is a simple change:

Once you’ve confirmed the library is static, go to the Build Phases for the app target in Xcode. Remove this library from any build phase named “Copy Files” or “Embed Frameworks.” The library should remain in the “Link Binary with Libraries” section.

By default, when you add a library to a framework or app target, Xcode chooses “Embed & Sign”. Was
the case for me as well here.

Change the library setting to 'Do Not Embed'

Change that to “Do Not Embed”, and you’re golden.

The result is that the framework is removed from the 'Embed Frameworks' build phase, but not the 'Link Binary With Libraries' phase; you can also perform this step manually, esp. if your Xcode version doesn't show the convenient 'Do Not Embed' dropdown

See also:

Open Magit for Current Repository from the Terminal

I’m having all my project in git repositories. And since I discovered the magic of Magit in Emacs, I sometimes want to have a familiar, interactive interface to select hunks for a commit without having to fire up a proper GUI app for stuff that I don’t already edit in Emacs.

In line with my recent idea to connect Finder and Emacs dired, I figured it might be nice to have a bash/zsh alias that visits the current directory in Emacs using Magit. (I use iTerm 2 as my terminal emulator of choice, not Emacs.)

With the emacsclient program, I can -e evaluate Lisp code from the shell. And with (magit-status PATH-TO-REPO), I can show the git repository at pwd in Magit.

After a short web search, I discovered some more elaborate aliases on Reddit and ended up combining the emacsclient invocation with a osascript call to bring Emacs to the foreground. The resulting alias is:

alias magit='emacsclient -a emacs -e "(magit-status \"$(git rev-parse --show-toplevel)\")"; if [[ -f `which osascript` ]]; then osascript -e "tell application \"Emacs\" to activate"; fi'

Split into a function for readability:

function magit () {
    git_root=$(git rev-parse --show-toplevel)
    emacsclient -a emacs \
        -e "(magit-status \"${git_root}\")"
    if [[ -f `which osascript` ]]; then
        osascript -e "tell application \"Emacs\" to activate"

If osascript is not found, e.g. on non-macOS systems, you won’t get an error. Looking at this for longer, it might make sense to extract an bringemacstofront alias in the future for reuse.

I don’t know how to write a “bring app window to front” for any flavor of Linux distro. Please share how you’d do that in the comments – I bet it’s different for every window manager of desktop environment anyway, but maybe there a universal X server command or something?

Two Improvements to Open macOS Finder Window in Emacs Dired + Automator Quick Action Downloads

In my previous post, I shared a function to fetch macOS Finder’s frontmost window path using AppleScript. The result was then opened in dired so you can import your Finder session into Emacs, so to speak.

Here are two improvements I discovered today when reading Sacha Chua’s Emacs News. (Downloads are at the very bottom.)

Redditor /u/cyanj shared that there’s a ns-do-applescript function we can use instead of calling out to the shell, spawning a new process. And it definitely feels faster!

If you want that, too, replace ct/finder-path with this:

(defun ct/finder-path ()
  "Return path of the frontmost Finder window, or the empty string.

Asks Finder for the path using AppleScript via `osascript', so
  this can take a second or two to execute."
  (let ($applescript $result)
    ;; Script via:
    (setq $applescript "tell application \"Finder\" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)")
    (setq $result (ns-do-applescript $applescript))
    (if $result
        (string-trim $result)

ns-do-applescript returns the script’s string result, if possible, which I also find more straight-forward than creating a temporary background buffer and capturing contents there. It works in my emacs-head@28 via homebrew. If you build Emacs from source, check if the ns-do-applescript function is available at all.

If you have this in your init.el, wrap everything in (when (memq window-system '(mac ns)) ...) as usual so you don’t accidentally try to execute the ns- prefixed function on e.g. Linux.

The post on Reddit there originally pointed to Álvaro Ramírez’s “macOS: Show in Finder / Show in Emacs” article, which introduces another enhancement to my set-up.

Up until that point, I got this Finder interop:

  • Reveal current file/directory that I see in Emacs in Finder
  • Open Finder’s frontmost window in Emacs dired

Both require Emacs being focused. So I’m telling Emacs to do everything: remote-control Finder, or fetch its window’s path.

Álvaro now introduces a macOS Service so that you can start in Finder and then tell Emacs to open this location. So you don’t have to switch to Emacs and fetch the Finder window. Tell, don’t ask, if you will!

His AppleScripts, copied here for preservation, are:

  1. Reveal file or folder in Emacs dired

     current_dir=$(dirname "$1")
     osascript -e 'tell application "Emacs" to activate'
     path/to/emacsclient --eval "(progn (dired \"$current_dir\") (dired-goto-file \"$1\"))"
  2. Visit (aka open) file in Emacs directly

     osascript -e 'tell application "Emacs" to activate'
     path/to/emacsclient --eval "(find-file \"$1\")"

Both bash scripts need to be wrapped into Automator Quick Actions (used to be called “Services”) that take files or folders from Finder as input. The shell script action takes the input as arguments, not the default as stdin. Check out Álvaro’s post for screenshots.

I tweaked the lookup a bit and added common (?) paths to look for emacsclient so you don’t have to hard-code the current value into the Automator action. Plus I added error handling if emacsclient can still not be found:

# You may have to add the location of your emacsclient to PATH for lookup to work

if [[ ! -f `which emacsclient` ]]; then
  >&2 echo "emacsclient not found"
  >&2 echo ""
  >&2 echo "Script needs tweaking. Looked in PATH: ${PATH}"
  exit 1

Since I have an Apple Developer ID, I figured I might as well bundle these actions up for you and also sign them so macOS doesn’t complain about the downloaded script. In about 6 months or so, we’ll probably be transforming these Automator actions to workflows, which I actually am looking forward to.

Download Reveal & Open in Emacs workflows

Open macOS Finder Window in Emacs Dired

Quite often I have a directory open in macOS Finder that I want to open in Emacs dired, e.g. to mass-rename files, copy stuff to a remote machine via SSH, or what have you.

In my last post, for example, I used Gifox to create a GIF and then viewed the result in Finder. To add the GIF to my blog post, I needed to copy it over into the appropriate website project directory. Since I was writing that post in Emacs already, I wanted to quickly copy the file from my Downloads folder (my default scratchpad for output like screenshots and screen recordings, including GIFs) over to the blog post’s location.

Now there’s two ways to move the file you see in Finder to a directory that you have access to in Emacs:

  1. Either reveal the Emacs file/directory a new Finder window (I am using reveal-in-osx-finder.el for that) and then use Finder to copy stuff between the directories;
  2. or visit the Finder window’s directory in Emacs and use dired to copy or move the file from one pane to the other.

Up until today, I couldn’t do the latter, and I quite like the experience of copying files between two dired panes, so this post documents how I ended up using AppleScript to fetch the Finder window’s location and jump to the path via dired.

Get Finder window location via AppleScript

I have a helper function to cd to the frontmost Finder window in the terminal for ages, based on a 2013 post by Brett Terpstra (of course):

# Part of my bash/zsh aliases file:
cdf() {
  target=`osascript -e 'tell application "Finder" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)'`
  if [ "$target" != "" ]; then
    cd "$target"; pwd
    echo 'No Finder window found' >&2

This calls AppleScript from the command line to ask for the frontmost window’s path.

This can be called from within Emacs, too, and I wrote a helper for that. Initially, I did overcomplicate everything and reacted to the exit code in case of AppleScript command failure instead of using Brett’s approach to just return an empty string when there’s no Finder window.

I’ll be using the same approach to access the Finder window’s location from Emacs.

Use osascript in Emacs to talk to Finder

What follows is not onw but two versions of more or less the same thing: asking Finder for the location using a command line invocation from Emacs.

I share both with you so you (and I) learn some Elisp along the way, because although the approach I’ll show first is simpler, I found the stuff I learned in the approach I’ll share last very educating, too. (And I ended up adding a couple notes to my Zettelkasten to document what I learned.)

The simple approach: a literal translation of Brett’s code

I shared Brett’s original AppleScript-based shell function above already. It either produces an empty string when no Finder window is open, or a POSIX path to the window’s location.

To call the osascript command line utility from Emacs, I use call-process and pass the AppleScript as an argument:

(defun ct/finder-path ()
  "Return path of the frontmost Finder window, or the empty string.

Asks Finder for the path using AppleScript via `osascript', so
this can take a second or two to execute."
  (let ($applescript)
    (setq $applescript "tell application \"Finder\" to if (count of Finder windows) > 0 then get POSIX path of (target of front Finder window as text)")
      ;; Produce a list of process exit code and process output (from the temp buffer)
      (call-process "/usr/bin/osascript" nil (current-buffer) nil "-e" $applescript)
      (string-trim (buffer-string)))))

The trick here is to put both the STDOUT (and STDERR) output into a buffer. Wrapped in with-temp-buffer, this effectively captures the command output in an invisible background buffer and then returns the trimmed result as a string.

This is apparently the most simple approach to getting the string output of a shell command. Both shell-command and call-process are designed to insert the result in a buffer, e.g. to write a log, or insert the result right where you are typing. That’s probably useful when you want to e.g. select text and then call a program to act on the selection (e.g. uniq or tr, although there’s Emacs Lisp equivalents of course) and finally replace the selected text with the output from that command. – This focus on interactivity makes capturing the output as a string instead a bit awkward, though.

Update 2021-07-12: I’ve updated my script to use ns-do-applescript instead, which performs the same much quicker. See the follow-up post.

To open the path that’s returned here in dired (aka “jump” to the path), the following interactive function does the real work and produces a “No Finder window found” message just like Brett’s original shell function:

(defun ct/dired-finder-path ()
  (let (($path (ct/finder-path)))
    (if (string-equal "" $path)
        (message "No Finder window found.")
      (dired $path))))

Now let’s have a look at my original, more cumbersome approach for everyone’s education.

The more complex approach that handles error codes

I ended up here because my actual cdf command line helper function produced a different AppleScript code – one that didn’t default to the empty string when no Finder window was open, but produce an error instead. I actually don’t find this more but less user friendly than Brett’s code from 2013.

To look up the code, I used which cdf in my terminal. It turns out that some zsh plug-in I installed along the way overwrote the code by Brett, so I got this:

$ which cdf
cdf () {
	cd "$(pfd)"

$ which pfd
pfd () {
	osascript 2> /dev/null <<EOF
    tell application "Finder"
      return POSIX path of (insertion location as alias)
    end tell

There’s no error handling, so without a Finder window open, the AppleScript execution will fail with an error as we’ll see below.

Also interesting to note: The AppleScript execution here pipes in a heredoc string via STDIN. Note that Brett’s original, shorter version uses the -e parameter to specify the AppleScript code as a string.

Initially, I tried to use shell-command, which also supports the backwards-piping using a heredoc. On top, this Elisp function allows you to specify a STDOUT and a STDERR output buffer to capture success and failure in parallel.

But everyone (including the Emacs manual) told me to not use this for programmatic shell command executions, so I switched to the slightly more cumbersome call-process. Using a heredoc was awkward with call-process’s separation of command name and parameters, so Iended up passing the script as a parameter instead using -e, too.

An upside of call-process over shell-command is that it returns the exit code of the command execution. So (call-process ...) will return 0 for successful execution of the command, or the error code (e.g. 1) upon failure. That arguably can make reacting to success and failures a bit simpler since the exit code is available. But then you need to react to both the output and the error code, which adds complexity.

That’s why the resulting variant of my function ct/finder-path produces a tuple or list of values, e.g. (list 0 "/path/to/window") on success, or (list 1 "XYZ Error Message beep boop") on failure.

(defun ct/finder-path ()
  "Ask Finder for the path of the frontmost window using AppleScript
and return the exit code as the 1st list item and the path as
the 2nd list item of the result.

This uses `/usr/bin/osascript', so it can take a second or two.

The return value is of the following form:

For non-zero exist codes, you should treat the output as an
error message."
  (let ($applescript)
    ;; This concatenates the multi-line AppleScript into a string:
    (setq $applescript (mapconcat 'identity
                                  '("tell application \"Finder\""
                                    "  return POSIX path of (insertion location as alias)"
                                    "end tell")
      ;; Produce a list of process exit code and process output (from the temp buffer)
      (list (call-process "/usr/bin/osascript" nil (current-buffer) nil "-e" $applescript)
            (string-trim (buffer-string))))))

As a consequence, the actual “go to Finder window in dired” command unwraps has to work with the tuple of values and unpack the two elements to conditionally display a failure message:

(defun ct/dired-finder-path ()
  (let* (($result (ct/finder-path))
         ($exit-code (car $result))
         ($output (cadr $result)))
    (if (eq 0 $exit-code)
        (dired $output)
      (message "Fetching Finder window failed (maybe no window open?):\n%s" $output))))

The minibuffer then displays this when no Finder window is open at all:

"Fetching Finder window failed (maybe no window open?):
72:77: execution error: Finder got an error: AppleEvent handler failed. (-10000)"

Yes, including the quotation marks. Not the prettiest, but serviceable.

I got the idea to return both the exit code and the captured output in a list from StackOverflow. The original author there suggests to extract a wrapper for this kind of command line invocation that looks very useful:

(defun process-exit-code-and-output (program &rest args)
  "Run PROGRAM with ARGS and return the exit code and output in a list."
    (list (apply 'call-process program nil (current-buffer) nil args)
          ;; Note: I'm trimming the output to drop trailing newline!
          (string-trim (buffer-string)))))

(process-exit-code-and-output "which" "bash")
;; => (0 "/usr/bin/bash")

Since I didn’t end up using this approach and don’t act on the exit code at all, I didn’t bother to introduce this generalized process helper.

But it’s a neat idea that might come in handy (and which I hopefully remember later).

Limit the scope to macOS

To limit the availability of these functions to macOS (in case you also run Emacs on Linux or Windows), wrap the function defuns in:

(when (memq window-system '(mac ns))
  ;; paste defun's here

I have this in my init.el and when I’m on macOS, I can invoke ct/dired-finder-path just fine, but don’t even see this function on Linux.

NSTextView Bug: Automatically Scroll Insertion Point Into View Broken When Setting NSTextContainer's lineFragmentPadding to Zero

Today I found that NSTextView has a bug where it won’t automatically scroll to keep the insertion point visible when you hit Enter or Return to insert a line break.

Usually, a text view keeps the insertion point visible as soon as the user types. You can scroll away, but as soon as the text view content change via user input, the insertion point scrolls into view automatically.

Unless you set NSTextContainer.lineFragmentPadding to 0.0: then it still works for all characters but line breaks.

The text view's scroll position sticks to y:0.0 when you hit enter.

This means when you’re at the bottom edge and hit Enter, your insertion point will vanish from screen. The expected behavior is for the text view to scroll down a line or two.

Any positive value that’s not 0.0 will work – including 0.1, which isn’t even a visible pixel’s width.

So if you are customizing a NSTextView in your app and find that entering line breaks doesn’t automatically scroll anymore, check that the lineFragmentPadding isn’t set to 0.0! The default value is 5.0, by the way, and setting this to 0 is probably a bad idea for your user interface anyway. Some horizontal margin from the edge of the text view makes the overall appearance look way less cramped.

(Submitted to Apple as FB9302224)

The example code is so simple that I didn’t upload it to GitHub since it fits into 20 lines of code. So if your code looks something like this, you’ll notice the problem, at least on Big Sur:

class AppDelegate: NSObject, NSApplicationDelegate {
    @IBOutlet var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        let scrollView = NSTextView.scrollableTextView()
        scrollView.translatesAutoresizingMaskIntoConstraints = false

        // Add scroll view to window
        let containerView = window.contentView!
        containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[scrollView]|", options: [], metrics: nil, views: ["scrollView" : scrollView]))
        containerView.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[scrollView]|", options: [], metrics: nil, views: ["scrollView" : scrollView]))

        // Break scroll-insertion-point-to-visible
        let textView = scrollView.documentView as! NSTextView
        // textView.textContainer?.lineFragmentPadding = 0.1  // works
        textView.textContainer?.lineFragmentPadding = 0.0    // breaks

TextKit 2 Introduces Block-Based Layout

I’ve just watched the TextKit 2 WWDC video. Without hands-on experience, I cannot say much about the tech, but judging from the presentation, all of the changes sound like an amazing step forward to make rich text-based interfaces nicer to work with. I didn’t particularly enjoy implementing some things in TextKit 1.

One thing that sticks out to me is the focus on blocks. TextKit 2 now comes with more ways to affect the layout and intersperse things based on the notion of inserting block-level elements; also called ‘paragraphs’ in some contexts. But not in the way you and I talk about paragraphs of text; in the technical understanding, we also treat empty lines between paragraphs of text as a spacing block element, and that’s also called a paragraph … So that’s why I prefer ‘block’, to avoid confusion.

The surprising element to me here is that this is somewhat in line with the syntax to format Gemini web pages about which I wrote last week. It’s an improvement over Markdown’s (accidental) edge cases and the use of inline styles. Gemini limits these to bold and italic text.

It’s no wonder that the writing app Ulysses switched from a pure-ish Markdown with all it’s weird cases and parsing problems to a structured format they call ‘Markdown XL’ see a comparison) where you can safely operate on the block-level first without having to look for the line above, below, or whatever to affect the result. (This also means it was introducing a new file format, but at least you can export easily.)

In a lot of cases (in most cases, maybe?), Markdown’s quirks aren’t relevant to users. – Does a novelist really need the ability to indent lists 4 levels deep with 4 spaces per level, and then mix in even further indented code blocks in between list items, but also wants to manually apply line breaking and thus manual indentation of the wrapping paragraphs that should not become code blocks? That’s a simple case to test your Markdown editor’s compliance with. And I think it’s utterly irrelevant in practice. In 3 years, I haven’t received a single complaint that The Archive’s syntax highlighting doesn’t support 100% of the original Markdown’s capabilities. Could be that people just want to write notes with a list or quote here and there, and this is inherently limiting the problem scope.

So TextKit 2 is going to be available to developers, soon, when macOS 12 hits, and working with text blocks is going to be simpler in a couple of years. That’ll mean easier insertion, nay, injection of images and other non-text attachments. I do wonder if this in turn will make non-block based decorations harder, like line numbers. I’ll report my findings.

– To kick things off, I’ve changed all my blog posts that were tagged #textkit to also carry a new #textkit1 tag. I don’t want to separate TextKit 2 and TextKit 1 tags completely, since there is overlap, so describing a superset #textkit with two subsets, #textkit1 and #textkit2, makes most sense. I also need a catchy name for block-based markups. I think we’ll be seeing more of these.

Further info:

→ Blog Archive