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
9 changes: 9 additions & 0 deletions dhavnii/App/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,20 @@ internal enum RecordingState: Equatable {
@MainActor
@Observable
internal class AppState {
internal static let autoPasteEnabledDefaultsKey = "autoPasteEnabled"

internal var recordingState: RecordingState = .idle
internal var lastTranscription: String = ""
internal var hasMicrophonePermission: Bool = false
internal var hasAccessibilityPermission: Bool = false
internal var hasCompletedOnboarding: Bool = false
internal var autoPasteEnabled = UserDefaults.standard.object(
forKey: AppState.autoPasteEnabledDefaultsKey
) as? Bool ?? true {
didSet {
UserDefaults.standard.set(autoPasteEnabled, forKey: Self.autoPasteEnabledDefaultsKey)
}
}

/// Check if all required permissions are granted
internal var hasAllPermissions: Bool {
Expand Down
32 changes: 21 additions & 11 deletions dhavnii/Features/Clipboard/ClipboardManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,27 @@ class ClipboardManager {
func copyAndPasteIfPossible(_ text: String) {
// Always copy to clipboard (mandatory)
copyToClipboard(text)

if AXIsProcessTrusted() {
// Ensure OpenWispher is not the active app (stealing focus)
if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier {
NSApp.hide(nil)
}

// Delay to allow focus to settle/switch back
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.simulatePaste()
}

let hasAccessibilityPermission = AXIsProcessTrusted()

let frontmostBundleIdentifier = NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "unknown"
print(
"📋 Auto-paste decision: accessibility=\(hasAccessibilityPermission), frontmostApp=\(frontmostBundleIdentifier)"
)

guard hasAccessibilityPermission else {
print("⚠️ Auto-paste skipped: accessibility permission unavailable")
return
}

// Ensure OpenWispher is not the active app (stealing focus)
if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier {
NSApp.hide(nil)
}

// Delay to allow focus to settle/switch back
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in
self?.simulatePaste()
}
}
}
8 changes: 8 additions & 0 deletions dhavnii/Features/Permissions/PermissionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ internal class PermissionManager {
}
}
}

internal func accessibilityPermissionState(for reason: String) -> Bool {
let trusted = hasAccessibilityPermission
print(
"🔓 Accessibility permission queried for \(reason): cached=\(trusted), lastKnown=\(lastAccessibilityState), monitoring=\(isMonitoring)"
)
return trusted
}
Comment on lines +244 to +250

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if accessibilityPermissionState is used anywhere in the codebase

echo "=== Searching for usages of accessibilityPermissionState ==="
rg -n "accessibilityPermissionState" --type swift

echo ""
echo "=== Searching for direct AXIsProcessTrusted calls outside PermissionManager ==="
rg -n "AXIsProcessTrusted" --type swift -g '!*PermissionManager*'

Repository: maker-or/openwispher

Length of output: 454


Remove unused method or update ClipboardManager to use it.

The method accessibilityPermissionState(for:) is never called anywhere in the codebase. Meanwhile, ClipboardManager.swift at line 70 calls AXIsProcessTrusted() directly instead of using this centralized method. Either remove the dead code or update ClipboardManager to use the new method for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dhavnii/Features/Permissions/PermissionManager.swift` around lines 244 - 250,
The method accessibilityPermissionState(for:) is unused—either delete it or make
ClipboardManager use it instead of calling AXIsProcessTrusted() directly; to
keep a single source of truth, update ClipboardManager to call
PermissionManager.accessibilityPermissionState(for: "ClipboardManager") (or the
instance method on your PermissionManager object) and remove the direct
AXIsProcessTrusted() usage, ensuring any logging/behaviour still references
hasAccessibilityPermission, lastAccessibilityState and isMonitoring for
consistency.


/// Verify accessibility permission - use only the standard reliable API
private func verifyAccessibilityPermission() -> Bool {
Expand Down
15 changes: 14 additions & 1 deletion dhavnii/Features/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1216,7 +1216,7 @@ private struct FallbackSettingsCard: View {

private struct GeneralSettingsView: View {
@Binding var showingResetAlert: Bool
var appState: AppState
@Bindable var appState: AppState
var hotkeyManager: HotkeyManager?

@AppStorage("autoLaunchEnabled") private var autoLaunchEnabled = false
Expand All @@ -1243,6 +1243,17 @@ private struct GeneralSettingsView: View {
}
}

SettingsGroup(title: "Behavior") {
SettingsRow(
icon: "square.and.arrow.down.on.square", title: "Auto-paste to focused field",
subtitle: "Paste transcriptions automatically after copying them"
) {
Toggle("", isOn: $appState.autoPasteEnabled)
.toggleStyle(.switch)
.controlSize(.small)
}
}

// Hotkey
SettingsGroup(title: "Hotkey") {
SettingsRow(
Expand Down Expand Up @@ -1366,6 +1377,7 @@ private struct GeneralSettingsView: View {
private func resetApp() {
UserDefaults.standard.removeObject(forKey: "hasCompletedOnboarding")
UserDefaults.standard.removeObject(forKey: "autoLaunchEnabled")
UserDefaults.standard.removeObject(forKey: AppState.autoPasteEnabledDefaultsKey)
UserDefaults.standard.removeObject(forKey: "selectedTranscriptionProvider")
UserDefaults.standard.removeObject(forKey: "fallbackTranscriptionProvider")
UserDefaults.standard.removeObject(forKey: "transcriptionTimeoutSeconds")
Expand All @@ -1383,6 +1395,7 @@ private struct GeneralSettingsView: View {
appState.hasCompletedOnboarding = false
appState.lastTranscription = ""
appState.recordingState = .idle
appState.autoPasteEnabled = true

NSApplication.shared.terminate(nil)
}
Expand Down
14 changes: 12 additions & 2 deletions dhavnii/Features/Transcription/TranscriptionService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Foundation
internal class TranscriptionService {
private let audioRecorder = AudioRecorder()
private let clipboardManager = ClipboardManager()
private let permissionManager: PermissionManager

private var groqClient = GroqAPIClient()
private var elevenLabsClient = ElevenLabsAPIClient()
Expand All @@ -30,8 +31,13 @@ internal class TranscriptionService {
internal static let transcriptionSavedNotification = Notification.Name(
"OpenWispher.TranscriptionSaved")

internal init(appState: AppState, selectedProvider: TranscriptionProviderType = .groq) {
internal init(
appState: AppState,
permissionManager: PermissionManager,
selectedProvider: TranscriptionProviderType = .groq
) {
self.appState = appState
self.permissionManager = permissionManager
self.selectedProvider = selectedProvider
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down Expand Up @@ -218,7 +224,11 @@ internal class TranscriptionService {
return
}

clipboardManager.copyAndPasteIfPossible(transcription)
if appState.autoPasteEnabled {
clipboardManager.copyAndPasteIfPossible(transcription)
} else {
clipboardManager.copyToClipboard(transcription)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

historyManager?.saveTranscription(text: transcription, provider: provider)

Expand Down
6 changes: 5 additions & 1 deletion openwispher/openwispherApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,11 @@ private struct AppContentView: View {
savedProviderRaw.flatMap { TranscriptionProviderType(rawValue: $0) } ?? .groq

// Initialize transcription service with selected provider
let service = TranscriptionService(appState: appState, selectedProvider: selectedProvider)
let service = TranscriptionService(
appState: appState,
permissionManager: permissionManager,
selectedProvider: selectedProvider
)
service.historyManager = manager
transcriptionService = service

Expand Down