Files
lanework/KanbanQuickLook/BoardPreviewProvider.swift
T
rzen b18f7ca609 Space in Finder opens the board — a Quick Look preview extension that outlines a .kanban package
A board is a folder, and a folder previews as a folder. `KanbanQuickLook.appex` gives it a
document's preview instead: the board's name, its tint and its symbol, then its lanes in display
order with each one's card count and its first few card titles.

The reading is `BoardOutline` (Kanban/Storage), deliberately not `BoardLoader.load`. The loader
throws on a half-broken board — right for opening one, wrong for pressing Space, where the honest
answer is the part that reads; it visits every card and lists `attachments/` and counts `comments/`
inside each; and it carries trash, tombstone migration and the defect stream, none of which renders.
This walk never throws and is capped at every level (`BoardOutlineLimits`): 12 lanes shown of at
most 100 considered, 6 card titles per lane of at most 200 parsed, counts by readdir-plus-stat up to
2000 per lane and never a parse. It re-derives nothing that decides *what* the answer is —
`FrontmatterDocument` parses, `IntegrityRules.isIdentityShaped` says what a lane or a card is,
`BoardLoader.directoryCandidates` supplies the stray tolerance, `Ranks` supplies display order,
`Palette` resolves colours — only *how far to look*.

The reply is HTML, the one data-based reply that reflows: a Quick Look panel is resized by the
user and a board outline is a wrapping row of columns, so a drawing block baked at a fixed
`contentSize` would be the wrong size a moment later. It gets vector text, its own scrolling and
light/dark for free. The board tint is a wash under the title and a lane's edge accent — never
under text, because a preview has none of `ContrastMath`'s ink-picking machinery and should not
grow one.

The extension compiles `Kanban/Storage` whole, the `KanbanMobile` arrangement — the directory is
one unit in practice, so a narrower list is not on offer. `STORAGE_ONLY` is new: EchoLedger's
consumer sections speak the live store's vocabulary, and the phone's `#if os(macOS)` cannot exclude
them from a target that *is* macOS. Platform, and layer. Nothing else defines it.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
2026-08-09 02:37:10 -04:00

59 lines
3.3 KiB
Swift

import Foundation
import Quartz
import UniformTypeIdentifiers
/// **Space in Finder, on a `.kanban` package** — the whole of this extension's job.
///
/// A board is a folder, and a folder previews as a folder: an icon and nothing else. This gives it a
/// document's preview instead — its name, its tint, its lanes in order, each lane's card count and
/// its first few card titles — built from `BoardOutline.read`, the capped read-only walk that lives
/// beside the storage format it reads (Kanban/Storage/BoardOutline.swift).
///
/// ### Data-based, not view-based
///
/// `QLPreviewProvider` is the data-based half of the Quick Look preview API: the extension answers
/// with *content* (`QLPreviewReply`) and Quick Look owns the window. The view-based half
/// (`QLPreviewingController` on an `NSViewController`) would put a SwiftUI hierarchy on screen, and
/// this preview has no use for one — it is static by design (v1: no interactivity, no attachment or
/// image loading), so a view controller would be a lifecycle to keep honest in exchange for nothing.
///
/// The reply is **HTML**, which is the one supported data type that reflows: a Quick Look panel is
/// resized by the user and a board summary is a wrapping row of columns, so a fixed `contentSize`
/// drawing block or a rendered image would be the wrong shape the moment the panel is not the size
/// it was baked at. HTML also gets vector text, selectable text, its own scrolling, and light/dark
/// through `prefers-color-scheme` — all of which a `CGContext` reply would have to reinvent.
///
/// ### Where the work happens
///
/// Everything is inside the reply's data-creation block, which is where Apple's own documentation
/// puts it ("Heavy lifting should be done inside of the dataCreationBlock instead of when creating
/// the QLPreviewReply"): `providePreview` returns immediately with a size hint, Quick Look draws its
/// loading state at the right size, and the walk runs while it does.
///
/// ### Reading the board
///
/// The extension is sandboxed with read access to the URL it was handed and nothing else, which is
/// exactly what the walk needs — it opens `index.md` files under `request.fileURL` and never looks
/// outside the package. `BoardOutline.read` does not throw: a folder that is not a board, a board
/// with a broken `index.md`, a permissions race mid-walk all yield an outline of whatever *did* read,
/// so this method has no error path of its own and Quick Look never sees a failed preview where it
/// could have shown a name.
final class BoardPreviewProvider: QLPreviewProvider, QLPreviewingController {
func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply {
let boardRoot = request.fileURL
return QLPreviewReply(
dataOfContentType: .html,
contentSize: BoardPreviewPage.contentSizeHint
) { reply in
let outline = BoardOutline.read(boardRoot: boardRoot)
reply.stringEncoding = .utf8
// The panel's own title bar. Left empty, Quick Look uses the file name — which is the
// folder name, and therefore *wrong* for every board whose `title:` differs from it.
reply.title = outline.title
reply.attachments = BoardPreviewPage.attachments(for: outline)
return BoardPreviewPage.html(for: outline)
}
}
}