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(history_manager.clone());
// Initialize the shortcuts
shortcut::init_shortcuts(app_handle);
// Note: Shortcuts are NOT initialized here.
// 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)]
let signals = Signals::new(&[SIGUSR2]).unwrap();
@ -285,6 +287,7 @@ pub fn run() {
commands::open_app_data_dir,
commands::check_apple_intelligence_available,
commands::initialize_enigo,
commands::initialize_shortcuts,
commands::models::get_available_models,
commands::models::get_model_info,
commands::models::download_model,

View file

@ -1,5 +1,10 @@
import { useEffect, useState, useRef } from "react";
import { Toaster } from "sonner";
import { platform } from "@tauri-apps/plugin-os";
import {
checkAccessibilityPermission,
checkMicrophonePermission,
} from "tauri-plugin-macos-permissions-api";
import "./App.css";
import AccessibilityPermissions from "./components/AccessibilityPermissions";
import Footer from "./components/footer";
@ -21,6 +26,9 @@ function App() {
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | 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] =
useState<SidebarSection>("general");
const { settings, updateSetting } = useSettings();
@ -36,12 +44,15 @@ function App() {
checkOnboardingStatus();
}, []);
// Initialize Enigo and refresh audio devices when main app loads
// Initialize Enigo, shortcuts, and refresh audio devices when main app loads
useEffect(() => {
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
hasCompletedPostOnboardingInit.current = true;
commands.initializeEnigo().catch((e) => {
console.warn("Failed to initialize Enigo:", e);
Promise.all([
commands.initializeEnigo(),
commands.initializeShortcuts(),
]).catch((e) => {
console.warn("Failed to initialize:", e);
});
refreshAudioDevices();
refreshOutputDevices();
@ -77,10 +88,31 @@ function App() {
try {
// Check if they have any models available
const result = await commands.hasAnyModelsAvailable();
if (result.status === "ok") {
// If they have models/downloads, they're done. Otherwise start permissions step.
setOnboardingStep(result.data ? "done" : "accessibility");
const hasModels = result.status === "ok" && result.data;
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 {
// New user - start full onboarding
setIsReturningUser(false);
setOnboardingStep("accessibility");
}
} catch (error) {
@ -90,7 +122,9 @@ function App() {
};
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 = () => {

View file

@ -412,6 +412,19 @@ async initializeEnigo() : Promise<Result<null, string>> {
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>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_available_models") };

View file

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