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.
    .catchAndReturn(())
    .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

Oops?

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"
    fi
}

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:  https://brettterpstra.com/2013/02/09/quick-tip-jumping-to-the-finder-location-in-terminal/
    (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

     #!/bin/bash
     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

     #!/bin/bash
     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
PATH=~/bin:/usr/local/bin:/usr/bin:$PATH

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

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 Shortcuts.app 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
  else
    echo 'No Finder window found' >&2
  fi
}

This calls AppleScript from the command line to ask Finder.app 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)")
    (with-temp-buffer
      ;; 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 ()
  (interactive)
  (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
EOF
}

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:
`(list PROCESS-EXIT-CODE PROCESS-OUTPUT)'.

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")
                                  "\n"))
    (with-temp-buffer
      ;; 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 ()
  (interactive)
  (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."
  (with-temp-buffer
    (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:

@main
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.addSubview(scrollView)
        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:

Trash File from Emacs with Put-Back Enabled on macOS

I’ve enabled using the macOS system trash in Emacs to feel safer experimenting with dired and other file manipulation functions in Emacs.

This enables using the trash by default:

(setq delete-by-moving-to-trash t)

When this is enabled, move-file-to-trash is used to delete files. The manual reveals what’s affected, but only the function API docs from within Emacs tell the whole story (and are super hard to search online for whatever reason):

If the function system-move-file-to-trash is defined, call it with FILENAME as an argument.

Otherwise, if trash-directory is non-nil, move FILENAME to that directory.

Otherwise, trash FILENAME using the freedesktop.org conventions, like the GNOME, KDE and XFCE desktop environments. Emacs moves files only to “home trash”, ignoring per-volume trashcans.

So by default, the delete-by-moving-to-trash variable enablement does nothing.

Set the user-default trash path to make deletion move a file to trash:

(setq trash-directory "~/.Trash")  ;; fallback for `move-file-to-trash'

With this, all files are moved into the trash, but you cannot use the macOS ‘Put Back’ functionality. I expected an extended file attribute to control this, but it’s something else, apparently. Reading the attributes of files trashed by Finder doesn’t reveal anything.

But there’s a command-line utility called trash that can be told to use Finder’s functionality to move a file to the trash and enable put-back.

Successful trashing of a file, see the message in the minibuffer

So if we revisit the function docs, it says trash-directory is just a fall-back. The first attempt is made using the function system-move-file-to-trash if it exists. If we declare that function, we can use it to use the trash CLI app; and for good measure, we’ll limit it to macOS:

(when (memq window-system '(mac ns))
  (defun system-move-file-to-trash (path)
    "Moves file at PATH to the macOS Trash according to `move-file-to-trash' convention.

Relies on the command-line utility 'trash' to be installed.
Get it from:  <http://hasseg.org/trash/>"
    (shell-command (concat "trash -vF \"" path "\""
                           "| sed -e 's/^/Trashed: /'")
                   nil ;; Name of output buffer
                   "*Trash Error Buffer*")))

This just forwards the file path to the trash CLI utility and then prepends a string to format the output; trash -F uses the Finder trash functionality; trash -v returns the path name of the trashed file on success; and with the sed replacement, we get "Trashed: /path/to/file.txt". This output is printed in the minibuffer after deletion as you see in the screenshot.

References:

I'm Thinking About the Gemini Protocol a Lot

In the past couple of months, I’ve been thinking about the Gemini Protocol on and off. As a protocol, it’s a SSL/TLS-enabled text transfer protocol. The stuff it’s supposed to transfer is text files sorta similar to Markdown. And the client is supposed to render the result.

It sits somewhere between Gopher and HTTP.

If HTML is the language of the web we know via HTTP, Gemini’s text/gemini plain text result is the language of the Gemini protocol.

Unlike Markdown, which was designed to translate to HTML nicely and be human-readable at the same time, text/gemini doesn’t translate to HTML and is only made to be read the way it’s written; plus it’s made to be simpler to write parsers for.

I’m not thinking about the protocol implementation. I’m thinking about this plain text part.

I’m a huge fan of Markdown to write everything – my notes and my website content and my book manuscripts alike. But the text/gemini specs are captivating, too: they decide to not allow inline links at all. If you want to write a link, it must be written on one line per link. The whole plain text format is line-oriented. Here’s an example of a few links:

=> gemini://example.org/
=> gemini://example.org/ An example link
=> gemini://example.org/foo	Another example link at the same host
=> foo/bar/baz.txt	A relative link
=> 	gopher://example.org:70/1 A gopher link.

Clients typically seem to render this as a list of links where “An example link” is the click-able anchor text, and the target URL is not shown on screen. You know, just like HTML links, but more restrictive.

DistroTube.com in a HTTP browser to mimick the Gemini rendering

If you’re curious how that scales, check out Derek Taylor’s DistroTube.com page that is nowadays a rendered HTML output of his “Gemini Capsule”. (It was one of Derek’s videos that brought the topic to my attention.)

I am drawn to “Gemini” the topic, and text/gemini the format, because its promise of simplicity is still appealing. Just text; that’s nice. And the one-link-per-line policy resembles how I write my notes. Inline links are too noisy for my taste when I’m writing and consuming the plain text notes as they are. Rendering text to e.g. HTML is a different thing; but for notes, I often prefer to have an annotated reference per line.

The whole Gemini topic is some form of escapism, I think. And that’s not bad. Escaping the web of 2021 that is ridden with ads, full of cookie consent banners, and where … ‘weird’ platforms dominate the web traffic, it’s perfectly understandable that folks want to go someplace else. A Gemini Capsule might be just that thing.

It’s a nerdy past-time activtiy. You’ll find cool people in Gemini Space, for sure. But some say this is the future of the web – but the plain text world never appealed to the main stream users once GUIs game along. So don’t plan to transfer all your traffic over to gemini:// anytime soon, anticipating that the HTTP web is going to go away.

See also (in a list of links, one per line):


→ Blog Archive