After taking day 12 off from my Psion December Adventure, I returned on day 13, adding a few quality of life improvements to Reconnect, my modern Psion connectivity software for macOS—I’m planning to spend the rest of the month focused on this and the other tooling that keeps these 30 year old computers going in 2024.

Reconnect

Working on Reconnect feels slower than cranking out OPL but there remain a collection of little fixes that will improve my daily experience and, hopefully, make it a little more enjoyable for others to use.

Continuing my focus on the Transfers window, I plumbed through file sizes so I can show the number of bytes remaining in a transfer. This is particularly useful on low-memory devices like the Psions where it’s easy to inadvertently copy a file larger than the whole device’s storage—the extra context can help spot such a mistake early. I also added support for showing previews of completed downloads which I find helps in quickly identifying the correct screenshots for these write-ups.

Transfers, now with file sizes and previews!

It doesn’t feel like there’s much novelty in the underlying code itself, with the minor exception of the approach taken to generating the file previews. This necessitated wrapping QuickLook’s classic completion-block API as a new cancellable async function to make it easier to call from SwiftUI:

extension QLThumbnailGenerator {

    public func thumbnailRepresentation(fileAt url: URL,
                                        size: CGSize,
                                        scale: CGFloat,
                                        iconMode: Bool) async throws -> QLThumbnailRepresentation {
        let request = Request(fileAt: url, size: size, scale: scale, representationTypes: .thumbnail)
        request.iconMode = iconMode
        return try await withTaskCancellationHandler {
            try await withCheckedThrowingContinuation { continuation in
                generateRepresentations(for: request) { (thumbnail, type, error) in
                    if let error {
                        continuation.resume(throwing: error)
                        return
                    }
                    continuation.resume(returning: thumbnail!)
                }
            }
        } onCancel: {
            cancel(request)
        }
    }

}

While it looks convoluted, this code is actually relatively simple: it uses the platform-provided withCheckedThrowingContinuation function to convert the classic QLThumbnailGenerator.generateRepresentations completion-block API into an async call, and then wraps that with withTaskCancellationHandler which returns a cancellable Task that will call QLThumbnailGenerator.cancel.

It’s frustrating to have to massage platform-provided APIs in this way, but once it’s done, it’s easy to use in SwiftUI:

@MainActor
struct ThumbnailView: View {

    let url: URL
    let size: CGSize

    @State var image: NSImage? = nil

    var body: some View {
        Image(nsImage: image ?? NSWorkspace.shared.icon(forFile: url.path))
            .task {
                let thumbnail = try? await QLThumbnailGenerator.shared.thumbnailRepresentation(fileAt: url, size: size, scale: 2.0, iconMode: true)
                image = thumbnail?.nsImage
            }
    }

}

The .task view modifier ensures the new async API is run once when the view is first attached and cancelled when it’s dismissed (if necessary).

With these usability improvements in-place, I took some time to experiment with adding drag-and-drop support and improving multi-image file conversions—is it better to convert multi-image MBMs to gifs, tiffs, or a zipped directory of image files? Questions for day 14 and beyond.