Skip to content

Commit d33bd71

Browse files
committed
Add permission step to onboarding
1 parent e199ca6 commit d33bd71

File tree

8 files changed

+425
-21
lines changed

8 files changed

+425
-21
lines changed

src-tauri/src/commands/mod.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,34 @@ pub fn check_apple_intelligence_available() -> bool {
130130
false
131131
}
132132
}
133+
134+
/// Try to initialize Enigo (keyboard/mouse simulation).
135+
/// On macOS, this will return an error if accessibility permissions are not granted.
136+
#[specta::specta]
137+
#[tauri::command]
138+
pub fn initialize_enigo(app: AppHandle) -> Result<(), String> {
139+
use crate::input::EnigoState;
140+
141+
// Check if already initialized
142+
if app.try_state::<EnigoState>().is_some() {
143+
log::info!("Enigo already initialized");
144+
return Ok(());
145+
}
146+
147+
// Try to initialize
148+
match EnigoState::new() {
149+
Ok(enigo_state) => {
150+
app.manage(enigo_state);
151+
log::info!("Enigo initialized successfully after permission grant");
152+
Ok(())
153+
}
154+
Err(e) => {
155+
if cfg!(target_os = "macos") {
156+
log::warn!("Failed to initialize Enigo: {} (accessibility permissions may not be granted)", e);
157+
} else {
158+
log::warn!("Failed to initialize Enigo: {}", e);
159+
}
160+
Err(format!("Failed to initialize input system: {}", e))
161+
}
162+
}
163+
}

src-tauri/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,10 @@ fn show_main_window(app: &AppHandle) {
110110
}
111111

112112
fn initialize_core_logic(app_handle: &AppHandle) {
113-
// Initialize the input state (Enigo singleton for keyboard/mouse simulation)
114-
let enigo_state = input::EnigoState::new().expect("Failed to initialize input state (Enigo)");
115-
app_handle.manage(enigo_state);
113+
// Note: Enigo (keyboard/mouse simulation) is NOT initialized here.
114+
// The frontend is responsible for calling the `initialize_enigo` command
115+
// after onboarding completes. This avoids triggering permission dialogs
116+
// on macOS before the user is ready.
116117

117118
// Initialize the managers
118119
let recording_manager = Arc::new(
@@ -275,6 +276,7 @@ pub fn run() {
275276
commands::open_log_dir,
276277
commands::open_app_data_dir,
277278
commands::check_apple_intelligence_available,
279+
commands::initialize_enigo,
278280
commands::models::get_available_models,
279281
commands::models::get_model_info,
280282
commands::models::download_model,

src/App.tsx

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,53 @@
1-
import { useEffect, useState } from "react";
1+
import { useEffect, useState, useRef } from "react";
22
import { Toaster } from "sonner";
33
import "./App.css";
44
import AccessibilityPermissions from "./components/AccessibilityPermissions";
55
import Footer from "./components/footer";
6-
import Onboarding from "./components/onboarding";
6+
import Onboarding, { AccessibilityOnboarding } from "./components/onboarding";
77
import { Sidebar, SidebarSection, SECTIONS_CONFIG } from "./components/Sidebar";
88
import { useSettings } from "./hooks/useSettings";
9+
import { useSettingsStore } from "./stores/settingsStore";
910
import { commands } from "@/bindings";
1011

12+
type OnboardingStep = "accessibility" | "model" | "done";
13+
1114
const renderSettingsContent = (section: SidebarSection) => {
1215
const ActiveComponent =
1316
SECTIONS_CONFIG[section]?.component || SECTIONS_CONFIG.general.component;
1417
return <ActiveComponent />;
1518
};
1619

1720
function App() {
18-
const [showOnboarding, setShowOnboarding] = useState<boolean | null>(null);
21+
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
22+
null
23+
);
1924
const [currentSection, setCurrentSection] =
2025
useState<SidebarSection>("general");
2126
const { settings, updateSetting } = useSettings();
27+
const refreshAudioDevices = useSettingsStore(
28+
(state) => state.refreshAudioDevices
29+
);
30+
const refreshOutputDevices = useSettingsStore(
31+
(state) => state.refreshOutputDevices
32+
);
33+
const hasCompletedPostOnboardingInit = useRef(false);
2234

2335
useEffect(() => {
2436
checkOnboardingStatus();
2537
}, []);
2638

39+
// Initialize Enigo and refresh audio devices when main app loads
40+
useEffect(() => {
41+
if (onboardingStep === "done" && !hasCompletedPostOnboardingInit.current) {
42+
hasCompletedPostOnboardingInit.current = true;
43+
commands.initializeEnigo().catch((e) => {
44+
console.warn("Failed to initialize Enigo:", e);
45+
});
46+
refreshAudioDevices();
47+
refreshOutputDevices();
48+
}
49+
}, [onboardingStep, refreshAudioDevices, refreshOutputDevices]);
50+
2751
// Handle keyboard shortcuts for debug mode toggle
2852
useEffect(() => {
2953
const handleKeyDown = (event: KeyboardEvent) => {
@@ -51,25 +75,39 @@ function App() {
5175

5276
const checkOnboardingStatus = async () => {
5377
try {
54-
// Always check if they have any models available
78+
// Check if they have any models available
5579
const result = await commands.hasAnyModelsAvailable();
5680
if (result.status === "ok") {
57-
setShowOnboarding(!result.data);
81+
// If they have models/downloads, they're done. Otherwise start permissions step.
82+
setOnboardingStep(result.data ? "done" : "accessibility");
5883
} else {
59-
setShowOnboarding(true);
84+
setOnboardingStep("accessibility");
6085
}
6186
} catch (error) {
6287
console.error("Failed to check onboarding status:", error);
63-
setShowOnboarding(true);
88+
setOnboardingStep("accessibility");
6489
}
6590
};
6691

92+
const handleAccessibilityComplete = () => {
93+
setOnboardingStep("model");
94+
};
95+
6796
const handleModelSelected = () => {
6897
// Transition to main app - user has started a download
69-
setShowOnboarding(false);
98+
setOnboardingStep("done");
7099
};
71100

72-
if (showOnboarding) {
101+
// Still checking onboarding status
102+
if (onboardingStep === null) {
103+
return null;
104+
}
105+
106+
if (onboardingStep === "accessibility") {
107+
return <AccessibilityOnboarding onComplete={handleAccessibilityComplete} />;
108+
}
109+
110+
if (onboardingStep === "model") {
73111
return <Onboarding onModelSelected={handleModelSelected} />;
74112
}
75113

src/bindings.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,18 @@ async openAppDataDir() : Promise<Result<null, string>> {
350350
async checkAppleIntelligenceAvailable() : Promise<boolean> {
351351
return await TAURI_INVOKE("check_apple_intelligence_available");
352352
},
353+
/**
354+
* Try to initialize Enigo (keyboard/mouse simulation).
355+
* On macOS, this will return an error if accessibility permissions are not granted.
356+
*/
357+
async initializeEnigo() : Promise<Result<null, string>> {
358+
try {
359+
return { status: "ok", data: await TAURI_INVOKE("initialize_enigo") };
360+
} catch (e) {
361+
if(e instanceof Error) throw e;
362+
else return { status: "error", error: e as any };
363+
}
364+
},
353365
async getAvailableModels() : Promise<Result<ModelInfo[], string>> {
354366
try {
355367
return { status: "ok", data: await TAURI_INVOKE("get_available_models") };

0 commit comments

Comments
 (0)