openquack

SPEC-013 — Usage stats pane

Status: draft (M3) Owner: OpenQuackKit/Stats/ Last updated: 2026-04-30

Goal

Show the user how much OpenQuack has done for them: words dictated, audio processed, time saved versus typing. The product moment is opening Settings and seeing “47,328 words dictated this month — about 5 hours faster than typing,” with a quiet line beneath: these numbers never leave your Mac. Pride and motivation, on the user’s terms, no telemetry attached.

Non-goals

Public surface

public actor UsageStats {
    public init(store: UserDefaults = .standard)

    /// Whether new dictations contribute to the counters. Display gating
    /// lives in the app layer; this flag controls *recording* the data.
    public var trackingEnabled: Bool { get async }
    public func setTrackingEnabled(_ enabled: Bool) async

    /// Record one successful dictation. No-op when tracking is disabled.
    /// Word count uses the script-aware heuristic (see Behaviour).
    public func record(transcript: String, audioSeconds: TimeInterval) async

    /// Current totals — cheap to call; reads cached fields.
    public func snapshot() async -> UsageStatsSnapshot

    /// Wipe all counters. Irreversible. Confirm at the UI layer.
    public func reset() async

    /// Serialise current state for the "Export…" button.
    public func exportJSON() async throws -> Data
}

public struct UsageStatsSnapshot: Sendable, Codable {
    public let wordsDictated: Int
    public let audioSeconds: TimeInterval
    public let dictationCount: Int
    public let firstRecordedAt: Date?

    /// time_saved = words / typing_wpm × 60 − audio_seconds, floored at 0.
    public func timeSaved(typingWordsPerMinute: Int) -> TimeInterval {
        let typingSeconds = Double(wordsDictated) / Double(typingWordsPerMinute) * 60
        return max(0, typingSeconds - audioSeconds)
    }
}

Behaviour

Persistence

UserDefaults under com.openquack.stats.* keys: wordsDictated, audioSeconds, dictationCount, firstRecordedAt, trackingEnabled. Atomic writes; one round-trip per record call. No SQLite, no JSONL — the working set is four scalars and the data plane stays trivial.

If a future spec adds per-day buckets for charts, escalate to a JSONL append-log under ~/Library/Application Support/OpenQuack/stats.jsonl. Out of scope for M3.

Settings UX

New Settings → Stats pane.

Privacy

OpenQuack’s privacy contract (VISION.md §Privacy contract) forbids audio and transcripts leaving the machine, and bans default-on telemetry. Stats-tracking complies:

Open questions

References