Skip to content

Commit 211462e

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

File tree

8 files changed

+417
-13
lines changed

8 files changed

+417
-13
lines changed

src-tauri/src/commands/mod.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,30 @@ pub fn check_apple_intelligence_available() -> bool {
130130
false
131131
}
132132
}
133+
134+
/// Try to initialize Enigo (keyboard/mouse simulation) after accessibility permissions are granted.
135+
/// This allows the app to gain paste functionality without requiring a restart.
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+
log::warn!("Failed to initialize Enigo: {}", e);
156+
Err(format!("Failed to initialize input system: {}", e))
157+
}
158+
}
159+
}

src-tauri/src/lib.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,24 @@ fn show_main_window(app: &AppHandle) {
111111

112112
fn initialize_core_logic(app_handle: &AppHandle) {
113113
// 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);
114+
// On macOS, we skip this at startup to avoid triggering the permission dialog.
115+
// The frontend will call initialize_enigo after the user grants permission.
116+
// On other platforms, we initialize immediately since no permission is needed.
117+
#[cfg(not(target_os = "macos"))]
118+
{
119+
match input::EnigoState::new() {
120+
Ok(enigo_state) => {
121+
app_handle.manage(enigo_state);
122+
}
123+
Err(e) => {
124+
log::warn!(
125+
"Failed to initialize input state (Enigo): {}. \
126+
Paste functionality may be unavailable.",
127+
e
128+
);
129+
}
130+
}
131+
}
116132

117133
// Initialize the managers
118134
let recording_manager = Arc::new(
@@ -275,6 +291,7 @@ pub fn run() {
275291
commands::open_log_dir,
276292
commands::open_app_data_dir,
277293
commands::check_apple_intelligence_available,
294+
commands::initialize_enigo,
278295
commands::models::get_available_models,
279296
commands::models::get_model_info,
280297
commands::models::download_model,

src/App.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@ 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";
99
import { commands } from "@/bindings";
1010

11+
type OnboardingStep = "accessibility" | "model" | "done";
12+
1113
const renderSettingsContent = (section: SidebarSection) => {
1214
const ActiveComponent =
1315
SECTIONS_CONFIG[section]?.component || SECTIONS_CONFIG.general.component;
1416
return <ActiveComponent />;
1517
};
1618

1719
function App() {
18-
const [showOnboarding, setShowOnboarding] = useState<boolean | null>(null);
20+
const [onboardingStep, setOnboardingStep] = useState<OnboardingStep | null>(
21+
null
22+
);
1923
const [currentSection, setCurrentSection] =
2024
useState<SidebarSection>("general");
2125
const { settings, updateSetting } = useSettings();
@@ -51,25 +55,39 @@ function App() {
5155

5256
const checkOnboardingStatus = async () => {
5357
try {
54-
// Always check if they have any models available
55-
const result = await commands.hasAnyModelsAvailable();
58+
// Check if they have any models or are using cloud transcription
59+
const result = await commands.hasAnyModelsOrDownloads();
5660
if (result.status === "ok") {
57-
setShowOnboarding(!result.data);
61+
// If they have models/downloads, they're done. Otherwise start permissions step.
62+
setOnboardingStep(result.data ? "done" : "accessibility");
5863
} else {
59-
setShowOnboarding(true);
64+
setOnboardingStep("accessibility");
6065
}
6166
} catch (error) {
6267
console.error("Failed to check onboarding status:", error);
63-
setShowOnboarding(true);
68+
setOnboardingStep("accessibility");
6469
}
6570
};
6671

72+
const handleAccessibilityComplete = () => {
73+
setOnboardingStep("model");
74+
};
75+
6776
const handleModelSelected = () => {
6877
// Transition to main app - user has started a download
69-
setShowOnboarding(false);
78+
setOnboardingStep("done");
7079
};
7180

72-
if (showOnboarding) {
81+
// Still checking onboarding status
82+
if (onboardingStep === null) {
83+
return null;
84+
}
85+
86+
if (onboardingStep === "accessibility") {
87+
return <AccessibilityOnboarding onComplete={handleAccessibilityComplete} />;
88+
}
89+
90+
if (onboardingStep === "model") {
7391
return <Onboarding onModelSelected={handleModelSelected} />;
7492
}
7593

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) after accessibility permissions are granted.
355+
* This allows the app to gain paste functionality without requiring a restart.
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)