fix first launch on macos (#690)

This commit is contained in:
CJ Pais 2026-02-01 11:16:13 +08:00 committed by GitHub
parent 6827fb51d4
commit 9b361f661b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 96 additions and 15 deletions

View file

@ -164,3 +164,28 @@ pub fn initialize_enigo(app: AppHandle) -> Result<(), String> {
} }
} }
} }
/// Marker state to track if shortcuts have been initialized.
pub struct ShortcutsInitialized;
/// Initialize keyboard shortcuts.
/// On macOS, this should be called after accessibility permissions are granted.
/// This is idempotent - calling it multiple times is safe.
#[specta::specta]
#[tauri::command]
pub fn initialize_shortcuts(app: AppHandle) -> Result<(), String> {
// Check if already initialized
if app.try_state::<ShortcutsInitialized>().is_some() {
log::debug!("Shortcuts already initialized");
return Ok(());
}
// Initialize shortcuts
crate::shortcut::init_shortcuts(&app);
// Mark as initialized
app.manage(ShortcutsInitialized);
log::info!("Shortcuts initialized successfully");
Ok(())
}

View file

@ -134,8 +134,10 @@ fn initialize_core_logic(app_handle: &AppHandle) {
app_handle.manage(transcription_manager.clone()); app_handle.manage(transcription_manager.clone());
app_handle.manage(history_manager.clone()); app_handle.manage(history_manager.clone());
// Initialize the shortcuts // Note: Shortcuts are NOT initialized here.
shortcut::init_shortcuts(app_handle); // The frontend is responsible for calling the `initialize_shortcuts` command
// after permissions are confirmed (on macOS) or after onboarding completes.
// This matches the pattern used for Enigo initialization.
#[cfg(unix)] #[cfg(unix)]
let signals = Signals::new(&[SIGUSR2]).unwrap(); let signals = Signals::new(&[SIGUSR2]).unwrap();
@ -285,6 +287,7 @@ pub fn run() {
commands::open_app_data_dir, commands::open_app_data_dir,
commands::check_apple_intelligence_available, commands::check_apple_intelligence_available,
commands::initialize_enigo, commands::initialize_enigo,
commands::initialize_shortcuts,
commands::models::get_available_models, commands::models::get_available_models,
commands::models::get_model_info, commands::models::get_model_info,
commands::models::download_model, commands::models::download_model,

View file

@ -1,5 +1,10 @@
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { Toaster } from "sonner"; import { Toaster } from "sonner";
import { platform } from "@tauri-apps/plugin-os";
import {
checkAccessibilityPermission,
checkMicrophonePermission,
} from "tauri-plugin-macos-permissions-api";
import "./App.css"; import "./App.css";
import AccessibilityPermissions from "./components/AccessibilityPermissions"; import AccessibilityPermissions from "./components/AccessibilityPermissions";
import Footer from "./components/footer"; import Footer from "./components/footer";
@ -21,6 +26,9 @@ function App() {
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>( const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
null, null,
); );
// Track if this is a returning user who just needs to grant permissions
// (vs a new user who needs full onboarding including model selection)
const [isReturningUser, setIsReturningUser] = useState(false);
const [currentSection, setCurrentSection] = const [currentSection, setCurrentSection] =
useState<SidebarSection>("general"); useState<SidebarSection>("general");
const { settings, updateSetting } = useSettings(); const { settings, updateSetting } = useSettings();
@ -36,12 +44,15 @@ function App() {
checkOnboardingStatus(); checkOnboardingStatus();
}, []); }, []);
// Initialize Enigo and refresh audio devices when main app loads // Initialize Enigo, shortcuts, and refresh audio devices when main app loads
useEffect(() => { useEffect(() => {
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) { if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
hasCompletedPostOnboardingInit.current = true; hasCompletedPostOnboardingInit.current = true;
commands.initializeEnigo().catch((e) => { Promise.all([
console.warn("Failed to initialize Enigo:", e); commands.initializeEnigo(),
commands.initializeShortcuts(),
]).catch((e) => {
console.warn("Failed to initialize:", e);
}); });
refreshAudioDevices(); refreshAudioDevices();
refreshOutputDevices(); refreshOutputDevices();
@ -77,10 +88,31 @@ function App() {
try { try {
// Check if they have any models available // Check if they have any models available
const result = await commands.hasAnyModelsAvailable(); const result = await commands.hasAnyModelsAvailable();
if (result.status === "ok") { const hasModels = result.status === "ok" && result.data;
// If they have models/downloads, they're done. Otherwise start permissions step.
setOnboardingStep(result.data ? "done" : "accessibility"); if (hasModels) {
// Returning user - but check if they need to grant permissions on macOS
setIsReturningUser(true);
if (platform() === "macos") {
try {
const [hasAccessibility, hasMicrophone] = await Promise.all([
checkAccessibilityPermission(),
checkMicrophonePermission(),
]);
if (!hasAccessibility || !hasMicrophone) {
// Missing permissions - show accessibility onboarding
setOnboardingStep("accessibility");
return;
}
} catch (e) {
console.warn("Failed to check permissions:", e);
// If we can't check, proceed to main app and let them fix it there
}
}
setOnboardingStep("done");
} else { } else {
// New user - start full onboarding
setIsReturningUser(false);
setOnboardingStep("accessibility"); setOnboardingStep("accessibility");
} }
} catch (error) { } catch (error) {
@ -90,7 +122,9 @@ function App() {
}; };
const handleAccessibilityComplete = () => { const handleAccessibilityComplete = () => {
setOnboardingStep("model"); // Returning users already have models, skip to main app
// New users need to select a model
setOnboardingStep(isReturningUser ? "done" : "model");
}; };
const handleModelSelected = () => { const handleModelSelected = () => {

View file

@ -412,6 +412,19 @@ async initializeEnigo() : Promise<Result<null, string>> {
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };
} }
}, },
/**
* Initialize keyboard shortcuts.
* On macOS, this should be called after accessibility permissions are granted.
* This is idempotent - calling it multiple times is safe.
*/
async initializeShortcuts() : Promise<Result<null, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("initialize_shortcuts") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getAvailableModels() : Promise<Result<ModelInfo[], string>> { async getAvailableModels() : Promise<Result<ModelInfo[], string>> {
try { try {
return { status: "ok", data: await TAURI_INVOKE("get_available_models") }; return { status: "ok", data: await TAURI_INVOKE("get_available_models") };

View file

@ -68,12 +68,15 @@ const AccessibilityOnboarding: React.FC<AccessibilityOnboardingProps> = ({
checkMicrophonePermission(), checkMicrophonePermission(),
]); ]);
// If accessibility is granted, initialize Enigo // If accessibility is granted, initialize Enigo and shortcuts
if (accessibilityGranted) { if (accessibilityGranted) {
try { try {
await commands.initializeEnigo(); await Promise.all([
commands.initializeEnigo(),
commands.initializeShortcuts(),
]);
} catch (e) { } catch (e) {
console.warn("Failed to initialize Enigo:", e); console.warn("Failed to initialize after permission grant:", e);
} }
} }
@ -118,9 +121,12 @@ const AccessibilityOnboarding: React.FC<AccessibilityOnboardingProps> = ({
if (accessibilityGranted && prev.accessibility !== "granted") { if (accessibilityGranted && prev.accessibility !== "granted") {
newState.accessibility = "granted"; newState.accessibility = "granted";
// Initialize Enigo when accessibility is granted // Initialize Enigo and shortcuts when accessibility is granted
commands.initializeEnigo().catch((e) => { Promise.all([
console.warn("Failed to initialize Enigo:", e); commands.initializeEnigo(),
commands.initializeShortcuts(),
]).catch((e) => {
console.warn("Failed to initialize after permission grant:", e);
}); });
} }