Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Release

on:
push:
branches: [main]

permissions:
contents: write

jobs:
release:
name: Build and release
runs-on: macos-latest

steps:
- uses: actions/checkout@v4

- uses: swift-actions/setup-swift@v3
- name: Get swift version
run: swift --version # Swift 6.2

- name: Build (release)
run: swift build -c release

- name: Create app bundle
run: |
mkdir -p Wisp.app/Contents/MacOS
mkdir -p Wisp.app/Contents/Resources
cp .build/release/Wisp Wisp.app/Contents/MacOS/
cp Support/Info.plist Wisp.app/Contents/

# ---------------------------------------------------------------------------
# Optional signing — only runs when the MACOS_CERTIFICATE secret is set.
# To enable:
# 1. Export your Developer ID Application certificate as a .p12 file
# 2. Base64-encode it: base64 -i cert.p12 | pbcopy
# 3. Add GitHub secrets:
# MACOS_CERTIFICATE — base64-encoded .p12
# MACOS_CERTIFICATE_PWD — .p12 export password
# MACOS_SIGNING_IDENTITY — e.g. "Developer ID Application: Your Name (TEAMID)"
# ---------------------------------------------------------------------------
- name: Import signing certificate
if: ${{ secrets.MACOS_CERTIFICATE != '' }}
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
KEYCHAIN_PWD="$(openssl rand -base64 32)"
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"

echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12"
security import "$RUNNER_TEMP/certificate.p12" \
-k "$KEYCHAIN_PATH" \
-P "$MACOS_CERTIFICATE_PWD" \
-T /usr/bin/codesign
security list-keychain -d user -s "$KEYCHAIN_PATH"
security set-key-partition-list \
-S apple-tool:,apple: -s -k "$KEYCHAIN_PWD" "$KEYCHAIN_PATH"

- name: Sign app bundle
if: ${{ secrets.MACOS_CERTIFICATE != '' }}
env:
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
run: |
codesign \
--deep --force --verify --verbose \
--sign "$MACOS_SIGNING_IDENTITY" \
--entitlements Wisp.entitlements \
--options runtime \
Wisp.app

- name: Create DMG
run: |
hdiutil create \
-volname "Wisp" \
-srcfolder Wisp.app \
-ov -format UDZO \
Wisp.dmg

- name: Create PKG (for Jamf / MDM deployment)
run: |
pkgbuild \
--component Wisp.app \
--install-location /Applications \
Wisp.pkg

- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
VERSION="$(date +%Y-%m-%d)-${GITHUB_SHA::7}"
gh release create "$VERSION" \
--title "Wisp $VERSION" \
--notes "Built from [\`${GITHUB_SHA::7}\`]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/commit/$GITHUB_SHA)" \
--latest \
Wisp.dmg \
Wisp.pkg
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Run tests
runs-on: macos-latest

steps:
- uses: actions/checkout@v4

- uses: swift-actions/setup-swift@v3
- name: Get swift version
run: swift --version # Swift 6.2

- name: Run tests
run: swift test
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Auto-generated from all feature plans. Last updated: 2026-03-28
- Swift 6.2 with strict concurrency checking + AppKit (NSPanel, Core Animation), WhisperKit (existing) (002-status-indicator-gui)
- Swift 6.2 with strict concurrency checking enabled + WhisperKit (existing), KeyboardShortcuts 2.x (Sindre Sorhus — re-add to Package.swift), AVFoundation (existing), CoreAudio (system framework), Apple FoundationModels (existing), AppKit + SwiftUI (003-config-screen)
- UserDefaults — two explicit keys (`selectedMicrophoneUID`, `cleanupPrompt`); hotkey managed automatically by KeyboardShortcuts library (003-config-screen)
- Swift 6.1+ with strict concurrency checking enabled + AppKit (NSWindow, NSMenu), SwiftUI (List, Button), Foundation (Codable, JSONEncoder/Decoder, FileManager) (004-transcription-log)
- JSON file — `~/Library/Application Support/Wisp/transcription-log.json` (004-transcription-log)

- Swift 5.9+ with strict concurrency checking + WhisperKit (Argmax), KeyboardShortcuts (Sindre Sorhus), AppKi (001-core-dictation-flow)

Expand All @@ -25,10 +27,10 @@ tests/
Swift 5.9+ with strict concurrency checking: Follow standard conventions

## Recent Changes
- 004-transcription-log: Added Swift 6.1+ with strict concurrency checking enabled + AppKit (NSWindow, NSMenu), SwiftUI (List, Button), Foundation (Codable, JSONEncoder/Decoder, FileManager)
- 003-config-screen: Added Swift 6.2 with strict concurrency checking enabled + WhisperKit (existing), KeyboardShortcuts 2.x (Sindre Sorhus — re-add to Package.swift), AVFoundation (existing), CoreAudio (system framework), Apple FoundationModels (existing), AppKit + SwiftUI
- 002-status-indicator-gui: Added Swift 6.2 with strict concurrency checking + AppKit (NSPanel, Core Animation), WhisperKit (existing)

- 001-core-dictation-flow: Added Swift 5.9+ with strict concurrency checking + WhisperKit (Argmax), KeyboardShortcuts (Sindre Sorhus), AppKi

<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->
28 changes: 28 additions & 0 deletions Support/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?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">
<dict>
<key>CFBundleExecutable</key>
<string>Wisp</string>
<key>CFBundleIdentifier</key>
<string>com.wisp.Wisp</string>
<key>CFBundleName</key>
<string>Wisp</string>
<key>CFBundleDisplayName</key>
<string>Wisp</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSUIElement</key>
<true/>
<key>NSMicrophoneUsageDescription</key>
<string>Wisp needs microphone access to transcribe your speech.</string>
<key>NSAccessibilityUsageDescription</key>
<string>Wisp needs Accessibility access to paste transcribed text into the active text field.</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
15 changes: 15 additions & 0 deletions Wisp.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?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">
<dict>
<!-- Required for microphone access -->
<key>com.apple.security.device.audio-input</key>
<true/>
<!-- Required for CGEvent paste -->
<key>com.apple.security.temporary-exception.accessibility</key>
<true/>
<!-- Required for WhisperKit / Core ML JIT compilation -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>
20 changes: 18 additions & 2 deletions Wisp/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var menuBarController: MenuBarController?
private var notificationService: NotificationService?
private var overlayWindow: StatusOverlayWindow?
private var logStore = TranscriptionLogStore()
private var logWindow: LogWindow?

func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
Expand Down Expand Up @@ -48,6 +50,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
keyEquivalent: ","
)
)
menu.addItem(
NSMenuItem(
title: "Show Log",
action: #selector(showLog),
keyEquivalent: ""
)
)
menu.addItem(.separator())
menu.addItem(
NSMenuItem(
Expand All @@ -59,6 +68,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
statusItem?.menu = menu
}

@objc private func showLog() {
if logWindow == nil {
logWindow = LogWindow()
}
logWindow?.show(entries: logStore.entries)
}

@objc private func openPreferences() {
guard let store = preferencesStore, let mics = microphoneList else { return }
PreferencesWindow.show(preferences: store, microphoneList: mics)
Expand Down Expand Up @@ -294,8 +310,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
currentSession = nil

switch result {
case .completed:
break
case .completed(let text):
logStore.append(text: text)
case .discarded(let reason):
switch reason {
case .tooShort:
Expand Down
13 changes: 13 additions & 0 deletions Wisp/Models/TranscriptionLogEntry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

struct TranscriptionLogEntry: Codable, Identifiable, Sendable {
let id: UUID
let text: String
let timestamp: Date

init(id: UUID = UUID(), text: String, timestamp: Date = Date()) {
self.id = id
self.text = text
self.timestamp = timestamp
}
}
54 changes: 54 additions & 0 deletions Wisp/Models/TranscriptionLogStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import Foundation

// Persists transcription history to:
// ~/Library/Application Support/Wisp/transcription-log.json

@MainActor
final class TranscriptionLogStore {

private(set) var entries: [TranscriptionLogEntry] = []
private let url: URL

static var storageURL: URL {
let appSupport = FileManager.default.urls(
for: .applicationSupportDirectory,
in: .userDomainMask
).first!
return appSupport
.appendingPathComponent("Wisp", isDirectory: true)
.appendingPathComponent("transcription-log.json")
}

init(url: URL = TranscriptionLogStore.storageURL) {
self.url = url
self.entries = Self.load(from: url)
}

func append(text: String) {
let entry = TranscriptionLogEntry(text: text)
entries.insert(entry, at: 0)
if entries.count > 500 {
entries.removeLast()
}
save()
}

// MARK: - Private

private static func load(from url: URL) -> [TranscriptionLogEntry] {
guard let data = try? Data(contentsOf: url) else { return [] }
guard let decoded = try? JSONDecoder().decode([TranscriptionLogEntry].self, from: data)
else { return [] }
return decoded
}

private func save() {
let directory = url.deletingLastPathComponent()
try? FileManager.default.createDirectory(
at: directory,
withIntermediateDirectories: true
)
guard let data = try? JSONEncoder().encode(entries) else { return }
try? data.write(to: url, options: .atomic)
}
}
16 changes: 2 additions & 14 deletions Wisp/Services/NotificationService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,7 @@ final class NotificationService {
alert.informativeText = message
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")

// Run non-modally so it doesn't block the app
let window = NSWindow(
contentRect: .zero,
styleMask: [],
backing: .buffered,
defer: true
)
alert.beginSheetModal(for: window)

// Auto-dismiss after 3 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
window.close()
}
NSApp.activate(ignoringOtherApps: true)
alert.runModal()
}
}
Loading
Loading