All projects

Implementation reference · macOS 14+

Pingo

Pingo is a native macOS menu bar scratchpad for small HTTP requests. It validates the target, sends the request with URLSession, and keeps the response summary, headers, and text preview in one compact window.

Problem

Small requests without a full API workspace

A quick endpoint check still needs method selection, headers, an optional payload, clear transport state, and enough of the response to diagnose the result. Pingo keeps that loop in a menu bar window while bounding the amount of response data retained for the interface.

Implementation scope

From editable request to response preview

A MenuBarExtra owns one ScratchpadViewModel backed by URLSessionAPIRequestService. The view model validates the URL, parses line-based headers, creates an APIRequest, and starts a cancellable task. The service streams the response with URLSession.bytes(for:), retains at most 512 KiB for display, and returns status, headers, duration, byte counts, and UTF-8 state for presentation.

Runtime flow

From request fields to bounded output

UI state, request coordination, transport work, and response presentation each have a narrow responsibility. Cancellation crosses the view-model and service boundary through the retained task, while the display limit is enforced as the response bytes arrive.

01

Request input

Collect the fields

Method, HTTP(S) URL, line-based headers, and an optional UTF-8 body.

Output

Editable SwiftUI state

02

View model

Validate and prepare

Reject invalid targets, parse headers, prevent concurrent sends, and retain the task.

Output

APIRequest

03

Request service

Send and stream

Build URLRequest, apply timeouts, call URLSession.bytes(for:), and accept cancellation.

Output

Bounded response bytes

04

Response output

Summarize for display

Expose status, duration, content type, sorted headers, and at most 512 KiB of UTF-8 text.

Output

APIResponse + UI state

The implemented request path. Validation and task state stay in the view model; transport, timeouts, and bounded byte collection stay in the request service.

The essential loop

Stream a bounded response preview

The request service streams bytes from URLSession and stops retaining data at the display limit. The returned model records whether the response was truncated and whether the retained bytes form valid UTF-8 text.

swift
let startDate = Date()
let (bytes, response) = try await session.bytes(for: urlRequest)
let httpResponse = response as? HTTPURLResponse
let bodyByteCount = httpResponse.flatMap(Self.contentLength)
let bodyData = try await displayedBodyData(from: bytes)
let duration = Date().timeIntervalSince(startDate)
let body = Self.utf8String(
    from: bodyData.data,
    repairingTruncatedData: bodyData.isTruncated
)
let isBodyTruncated = bodyData.isTruncated
    || bodyByteCount.map { $0 > bodyData.data.count } == true

return APIResponse(
    statusCode: httpResponse?.statusCode ?? 0,
    headers: httpResponse?.allHeaderFields ?? [:],
    body: body ?? "",
    bodyByteCount: bodyByteCount,
    displayedBodyByteCount: bodyData.data.count,
    isBodyTruncated: isBodyTruncated,
    isBodyText: body != nil,
    duration: duration
)

Implementation notes

Cancellable state and bounded streaming

PingoApp presents a window-style MenuBarExtra and keeps a single ScratchpadViewModel in SwiftUI state. The app is an LSUIElement, uses App Sandbox with outgoing network access, and has no primary application window.

ScratchpadViewModel owns editable request state and presentation strings. It prevents concurrent sends, retains the current request task for cancellation, and maps cancellation, timeout, validation, and other errors to distinct UI states.

URLSessionAPIRequestService converts the value-type APIRequest into URLRequest, applies headers and an optional UTF-8 body, and measures the request around URLSession.bytes(for:).

The response stream is consumed one byte at a time until completion or the 512 KiB display limit. Truncated UTF-8 data is repaired only by removing up to four incomplete trailing bytes; otherwise non-text data is reported instead of decoded.

Sparkle starts with the app and provides update checks from the menu. Its feed URL and public verification key are stored in the app configuration.