Remote Bulk Editing Using Regexp with Emacs

A couple of days ago, I did maybe the weirdest and also most amazing thing on a remote machine thus far (with Emacs) and wanted to share the story.

So there’s this EmulationStation software that organizes roms with XML lists for metadata. I needed to change the <name> field of about 100 entries there to drop a numeric prefix. That’d be easy on my local computer with tools I know well, but I was accessing the device remotely via SSH and wanted to see what I could do.

I’m not very good at plain shell scripting which would’ve been an option, so I fired up Emacs to make a connection to the device.

For all intents and purposes, what you see from within Emacs locally is indistinguishable from what you see when logged into a remote machine. This helps with my effectivity, because then I can use the tools I know well to do things on remote machines, too. This is different from SSH’ing onto the machine and opening an editor there, because opening emacs/vi/nano on the remote machine would be loading that machine’s configuration. If you open your local Emacs and create a connection to the remote machine from there, your local configuration will be used. It’s just like live editing web pages via FTP as we did it in the 90’s.

The only exception are locally installed packages that interface with locally installed command-line tools (e.g. ag or ripgrep). The functions will be available, but executing these programs on the remote machine won’t work. The binary won’t be found.

So the game metadata files I needed to adjust are grouped into directories, one per system. That meant I needed to find all gamedata.xml files in all directories first. Then find the numerical prefixes within each (and replace them). I’m able to perform both tasks individually from the shell, but the combination would’ve already required some research, especially since my local machine has non-POSIX tools like fd installed that I got used to, while the remote device is a bare-bones Linux. I’m also used to grep replacements, which made the search-and-replace operation a bit cumbersome. But that wouldn’t be a problem with Emacs as you’ll see in a second.

A simple regex for the numerical prefixes is <name>\d\d\d\s, and the replacement string would be <name>. Emacs regular expressions are weird because you have to escape parens and brackets that are part of the PCRE syntax. There’s a snippet to help with that so you can write the syntax you know and have it translated to Emacs-escaped regex. I didn’t know of that, sadly, and experimented around until I found that <name>[0-9]\{0,3\} works (note the trailing space). TIL: The curly braces require escaping, and neither \d nor \s seem to work.

Now here are the steps I took:

  • Install wgrep package if you don’t have it already. It makes grep buffers writeable. That means you can perform changes to search result buffers in-place and save them in bulk.
  • Run M-x rgrep for recursive search:
    • Search for <name>
    • Specify the file wildcard as *.xml
    • Specify the root folder from which to start recursive search
  • In the results buffer that appears, hit w or M-x wgrep-change-to-wgrep-mode to make the grep results buffer writeable.
  • Run M-x replace-regex and replace <name>[0-9]\{0,3\} (again, note the trailing space) with <name>.
  • To save all changes to all matched files, hit C-c C-c.

And that’s it!

The magic piece here is that Emacs will open, modify, and save all files according to the “diff” you produce in wgrep or “writable grep” mode. I didn’t have to figure out how to search recursively for the pattern in all XML files and afterwards perform the replacement automatically.

On my Mac, I would’ve been able to achieve this 10+ years ago with TextMate (or BBEdit), too. Local changes never were much of a problem.

But with Emacs, I was able to do these changes remotely. The hardest part was figuring out how to escape what in the regex. (I used highlight-regexp to more or less interactively highlight matches.)

SwiftUI Requires Platform Knowledge On Top. Case Study: Fonts

SwiftUI very likely is the future of app development. But it cannot, on its own replace UIKit or AppKit. Not yet, maybe not next year, either.

Eventually you will have to drop down a level to implement a custom view, custom navigation, animation, window, or what have you. So for the time being, the two worlds of UIKit and SwiftUI co-exist and complement each other.

It gets weird when you try to (or need to) bridge genuine SwiftUI objects to make them available in UIKit. The other way around is expected, but just take a look at the many hoops the folks at Moving Parts had to jump through to get SwiftUI.Font metadata info into UIKit: https://movingparts.io/fonts-in-swiftui

And I’m not even talking about the weird stuff that uses private API.

My own SwiftUI experiments are both fun and challenging, don’t get me wrong. It’s good.

I’m worried for the next couple of years, though: if cross-platform apps are to become a thing, making them good and feature-complete on every platform requires, well, platform-specific expertise plus knowledge of SwiftUI plus knowledge to bridge these worlds. You’ll need to be fluent in SwiftUI, UIKit, and AppKit. Meanwhile, because you can get 60% or more of the app’s functionality in pure SwiftUI, you will not be exposed to UIKit and AppKit as much.

Well, maybe I’m a slow learner – but I absolutely needed to immerse myself into AppKit for years to become proficient and comfortable. The time it took to create my first app is ridiculous in hindsight. I’m not sure how to learn only the parts of AppKit that you need to complement SwiftUI, for example. I’d wager it’s impossible. Either you know how AppKit works for the most part, or you don’t. So any genuine time saving of SwiftUI for cross-platform app development won’t kick in for years. Same goes for UIKit.

What Do You Get When You Drag and Drop a PNG File From Finder Into an NSTextView?

In short: Image file drag and drop does only produce file URLs, either with security scope-able bookmarks or plain file paths.

Dragging an image file from Finder onto an NSTextView will trigger performDragOperation(_:). You get access to NSDraggingInfo there and can inspect available content. Its draggingPasteboard (shortened to pb here) contains the following data when executed on macOS 12 Monterey:

Code Value
pb.string(forType: .string) nil
pb.string(forType: .URL) nil
NSURL(from: pb) file:///.file/id=xyz123
pb.string(forType: .fileURL) file:///.file/id=xyz123
NSURL(from: pb) as URL? file:///path/to/a.png
URL(string: pb.string(
    forType: .fileURL)!)
file:///path/to/a.png

Note that the .URL and .fileURL pasteboard types were added in macOS 10.13. Before that, you had to use the NSURL.init(from:) helper, the docs say. The table shows it still works.

Also pay attention to bridging NSURL to Swift URL: this changes the file URL or security scoped bookmark URL to an absolute file path. Accessing a resource with a security scope using these won’t work, I believe. Haven’t tested that yet.

When inspecting all available pasteboard types, you get these:

> pb.types?.map { $0.rawValue }

["public.file-url",
 "CorePasteboardFlavorType 0x6675726C",
 "dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn",
 "NSFilenamesPboardType",
 "dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu",
 "Apple URL pasteboard type",
 "com.apple.finder.node"])

I have no clue what the dyn.-prefixed ones are supposed to be. "NSFilenamesPboardtype" can be interesting, and "public.file-url" reveals what the .fileURL pasteboard type actually is under the hood. (You could maybe add a shim and read the file URL type even in macOS 10.12 and earlier that way – but I haven’t verified if that’s true or if dragging a file on macOS 10.12 does not actually set the "public.file-url" pasteboard type.)

Also note that pb.availableType(from: [.string, .fileContents, .html, .png, .tiff]) will be nil. You would need to use the newer .fileURL type to get a non-nil result, even though NSURL.init(from:) works.

This also shows that there’s no actual image associated witht he drag operation, which may or may not surprise you.

As a bonus, if you inspect the string values of all available types, formatted for readability, you get:

> pb.types?.map { pb.string(forType: $0) }

- public.file-url
file:///.file/id=xyz123

- CorePasteboardFlavorType 0x6675726C
file:///.file/id=xyz123

- dyn.ah62d4rv4gu8y6y4grf0gn5xbrzw1gydcr7u1e3cytf2gn
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>/path/to/a.png</string>
</array>
</plist>

- NSFilenamesPboardType
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>/path/to/a.png</string>
</array>
</plist>

- dyn.ah62d4rv4gu8yc6durvwwaznwmuuha2pxsvw0e55bsmwca7d3sbwu
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>file:///.file/id=xyz123</string>
    <string></string>
</array>
</plist>

- Apple URL pasteboard type
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
    <string>file:///.file/id=xyz123</string>
    <string></string>
</array>
</plist>

- com.apple.finder.node
file:///.file/id=xyz123

Don't Use Sparkle 2.x With Carthage

An old code base was still using Sparkler v1.27.0. To test the transition to the latest 2.x branch, the one that allows Sandboxing and uses XPC services under the hood, I migrated that project.

Carthage's framework out-of-the-box doesn't work well with code signing

Carthage builds of Sparkle’s v2.x branch don’t work well, though. You would need to do a lot of manual re-signing, otherwise the code signing stage of your build will fails. It does with a simple test project that embeds and signs the framework.

A couple of years ago, manual signing was necessary, but not anymore. Since my M1 Monterey Mac forces me to use Xcode 13 anyway, I figured I might now just as well use SwiftPM – and that works like a charm!

So if you get an error like:

…/SparkleTestApp.app/Contents/Frameworks/Sparkle.framework/Versions/B: code object is not signed at all
In subcomponent: …/SparkleTestApp.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate
Command CodeSign failed with a nonzero exit code

… then you might want to try to switch to SwiftPM or git submodule for Sparkle. (CocoaPods should work, too, but adding that on top of/next to Carthage seems weird.)

In The Archive and the Word Counter, I’m currently using a git submodule-based approach. That’s because I adopted the XPC-based changes quite early and needed to switch between my fork with some adjustments and the upstream code. So this is probably the first time in, what, 4 years?, that I’m relying on the vanilla version of Sparkle again. This increases my confidence in the updater I ship quite a bit.

Use macOS System Selection Colors in LIN for Emacs Line-Selection UIs

The Apple’s Human Interface Guidelines have a list of named system colors for macOS. These color names can be used for Emacs “faces”, i.e. color-and-font settings of text.

LIN to the left in a (squished) email list, hl-mode to the right in regular text

Recently, this came in handy when Protesilaos Stavrou published his LIN package source code – it’s basically an extension of the built-in hl-mode, read: highlight-line-mode. The default highlights the current line. That’s kind of useful to find your insertion point more quickly. But it’s also used in selection interfaces to highlight the currently selected line: email modes like message-mode, notmuch, mu4e, and feed reader elfeed use this to show that the actions you can perform pertains to the currently focused or highlighted line. That’s where Prot’s LIN package comes into play: it helps distinguish between highlighting the line in text editing modes and highlighting the line in selection interfaces, so you can use different colors.

You could say that LIN adds a semantic layer on top of “highlight this line”: it can distinguish between “highlight this line of text” and “highlight the selection”.

Being on macOS, I wanted the system default selection color. Blue background with white foreground color.

I used this approach for hl-line-mode since May and wrote about this back then. I couldn’t use hl-line to highlight my line in text documents if I wanted to, though, because of the color choice. This new approach improves that thanks to LIN.

To use macOS’s named colors for selections as LIN’s background and foreground instead of specifying color values directly, tweak the two LIN faces:

(when (memq window-system '(mac ns))
  (set-face-attribute 'lin-hl nil
                      :background "selectedContentBackgroundColor")

  ;; To also override the foreground (see `lin-override-foreground'):
  (set-face-attribute 'lin-hl-override-fg nil
                      :foreground "alternateSelectedControlTextColor"
                      :background "selectedContentBackgroundColor"))

The cool part about named system colors on macOS is that they are “Dynamic System Colors”: in dark mode, they produce a different value than in light mode. That means you don’t need to pick dark and light colors individually. The same color names will work.

If you switch your OS or app’s appearance to dark mode, though, you need to effectively reload the colors. Unlike native macOS apps, Emacs won’t automatically use the correct color for the current appearance.

So you need to trigger an update of LIN’s faces to pick up the color values if you change the appearance from dark to light, or light to dark. macOS builds of Emacs have a hook for this, and you can perform a color update by adding a function to ns-system-appearance-change-functions:

(defun my-lin-macos-system-colors ()
  (when (memq window-system '(mac ns))
    (set-face-attribute 'lin-hl nil
                        :background "selectedContentBackgroundColor")

    ;; To also override the foreground (see `lin-override-foreground'):
    (set-face-attribute 'lin-hl-override-fg nil
                        :foreground "alternateSelectedControlTextColor"
                        :background "selectedContentBackgroundColor")))

(when (memq window-system '(mac ns))
  (add-hook 'ns-system-appearance-change-functions #'my-lin-macos-system-colors))

This information is now also part of LIN’s README and, if Prot’s past efforts to document his packages holds true, will also become part of the manual when you install this package.

Swift Pattern Matching Operator: Compiler Error when Specifying Enum Type and Case Name?

Today’s WTF moment with Swift is related to switch-case and the pattern matching operator, ~=.

Defining operator overloads for special cases can help to keep case statements readable. And I thought it’d be simple enough to quickly check the setup, but I was in for a surprise!

So let’s say you have an enum that is RawRepresentable and backed by some integer type. Take UInt16, for example, because we’re dealing with a C API today that stores some type attributes as unsigned short and we want a nice Swift API for that:

enum CType: UInt16 {
    case two = 2
}

Now you can compare CType.two.rawValue == 2, and you can replace the literal 2 with the C unsigned short reference, which is exposed to Swift as UInt16.

But the .rawValue suffix gets old quickly when you have many cases in CType and want to switch over the C struct’s type directly.

So you reach for the pattern matching operator to make this shorter:

func ~= (pattern: CType, value: UInt16) -> Bool {
    return pattern.rawValue == value
}

With this operator overload, you can now write CType.two ~= 2, and perform pattern matching in if-case and guard-case and switch-case statements.

The weird part if how Swift allows you to do that.

So my line of thought was to write case CType.two: to be verbose and help the Swift compiler find the correct operator overload.

switch UInt16(2) {
case 1: // ...
case CType.two: print("Matched!")
default: // ...
}

The thing is: this doesn’t even compile!

error: enum case ‘two’ is not a member of type ‘UInt16’

Well, yes, that’s right – the case is a member of CType, as I am plainly telling in the code.

I tried other variants to help the compiler figure out what this pattern I want to match against is. It turned out that variables work just fine with Xcode 13.1 RC, as do static variables on the UInt16 type:

let cTypeVar = CType.two

extension UInt16 {
    static var cTypeStatic: CType { .two }
}

switch UInt16(2) {
case 2: fallthrough                   // ✓  Compiles fine
case cTypeVar: fallthrough            // ✓  Compiles fine
case UInt16.cTypeStatic: fallthrough  // ✓  Compiles fine
case CType.two: fallthrough           // ✗  Does not compile
default: break
}

Basically everything works except the enum wrapper for known values I have. Even a static variable that returns the same type!

Some time later on Slack, Ian S. told me to try just case .two:

switch UInt16(2) {
case 2: fallthrough                   // ✓  Compiles fine
case cTypeVar: fallthrough            // ✓  Compiles fine
case UInt16.cTypeStatic: fallthrough  // ✓  Compiles fine
case /*CType*/.two: fallthrough       // ✓  Compiles fine?!?
default: break

For all intents and purposes, this appears to be a bug in Swift – the pattern matching operator overload works when I don’t specify the type name, as you can see in the last code example, but the compiler produces an error when the .two case is written with its fully qualified type? That doesn’t make sense; if anything, I could understand if the opposite were the case: if the compiler cannot figure out what .two refers to, but has a much easier time compiling the pattern matching case statement when I type out case CType.two:...

Have reported this as FB9724060.

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&shy;CurrentDrawing&shy;Appearance for me:

extension NSAppearanceCustomization {
    @discardableResult
    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.
    .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.


→ Blog Archive