Desktop app: API key check + typing fallback #4003
1 changed file+74−52
Modifieddesktop/main/index.js+74−52View fileUnifiedSplit
@@ -197,8 +197,23 @@ async function toggleRecording() {
197197
198198async function startRecording() {
199199 if (isRecording) return
200 isRecording = true
201200
201 // Check API key BEFORE recording — don't waste user's time
202 const engine = store.get('engine', 'whisper')
203 if (engine === 'whisper' && !store.get('whisperApiKey')) {
204 dialog.showMessageBox({
205 type: 'warning',
206 title: '48co — API Key Needed',
207 message: 'You need an OpenAI API key for voice transcription.',
208 detail: 'Right-click the tray icon → Settings → enter your API key.\n\nGet one at: platform.openai.com/api-keys\nCosts about $0.006 per minute of speech.',
209 buttons: ['Open Settings', 'OK'],
210 }).then(({ response }) => {
211 if (response === 0) createSettingsWindow()
212 })
213 return
214 }
215
216 isRecording = true
202217 updateTrayMenu()
203218
204219 // Tell renderer to start capturing audio (overlay handles mic access)
@@ -231,67 +246,74 @@ async function stopRecording() {
231246}
232247
233248// ─── Keyboard Simulation (type into focused app) ──────────────
234async function typeText(text) {
235 try {
236 const { keyboard, Key } = require('@nut-tree-fork/nut-js')
237 const { clipboard } = require('electron')
238
239 // Hide overlay BEFORE typing so it doesn't interfere with focus
240 if (overlayWindow && overlayWindow.isVisible()) {
241 overlayWindow.hide()
242 }
243
244 // Give the user's app a moment to regain focus
245 await new Promise(r => setTimeout(r, 150))
249// Uses nut-tree for keyboard simulation. If nut-tree isn't available,
250// falls back to clipboard copy + user notification.
251let nutTree = null
252try {
253 nutTree = require('@nut-tree-fork/nut-js')
254} catch {
255 console.warn('[48co] @nut-tree-fork/nut-js not available — will use clipboard fallback')
256}
246257
247 const speed = store.get('typingSpeed', 0)
258async function typeText(text) {
259 const { clipboard } = require('electron')
248260
249 if (speed === 0) {
250 // Instant mode: clipboard paste (fastest, most reliable)
251 const originalClipboard = clipboard.readText()
252 clipboard.writeText(text)
261 // Hide overlay BEFORE typing so it doesn't interfere with focus
262 if (overlayWindow && overlayWindow.isVisible()) {
263 overlayWindow.hide()
264 }
253265
254 // Ensure clipboard is ready
255 await new Promise(r => setTimeout(r, 100))
266 // Give the user's app a moment to regain focus
267 await new Promise(r => setTimeout(r, 150))
256268
257 // Simulate Ctrl+V / Cmd+V
258 if (process.platform === 'darwin') {
259 await keyboard.pressKey(Key.LeftSuper)
260 await keyboard.pressKey(Key.V)
261 await keyboard.releaseKey(Key.V)
262 await keyboard.releaseKey(Key.LeftSuper)
269 // Method 1: nut-tree keyboard simulation (preferred)
270 if (nutTree) {
271 try {
272 const { keyboard, Key } = nutTree
273 const speed = store.get('typingSpeed', 0)
274
275 if (speed === 0) {
276 // Instant mode: clipboard paste (fastest, most reliable)
277 const originalClipboard = clipboard.readText()
278 clipboard.writeText(text)
279 await new Promise(r => setTimeout(r, 100))
280
281 if (process.platform === 'darwin') {
282 await keyboard.pressKey(Key.LeftSuper)
283 await keyboard.pressKey(Key.V)
284 await keyboard.releaseKey(Key.V)
285 await keyboard.releaseKey(Key.LeftSuper)
286 } else {
287 await keyboard.pressKey(Key.LeftControl)
288 await keyboard.pressKey(Key.V)
289 await keyboard.releaseKey(Key.V)
290 await keyboard.releaseKey(Key.LeftControl)
291 }
292
293 setTimeout(() => {
294 try { clipboard.writeText(originalClipboard) } catch {}
295 }, 500)
263296 } else {
264 await keyboard.pressKey(Key.LeftControl)
265 await keyboard.pressKey(Key.V)
266 await keyboard.releaseKey(Key.V)
267 await keyboard.releaseKey(Key.LeftControl)
297 await keyboard.type(text)
268298 }
269299
270 // Restore original clipboard after paste completes
271 setTimeout(() => {
272 try { clipboard.writeText(originalClipboard) } catch {}
273 }, 500)
274 } else {
275 // Character-by-character typing mode
276 await keyboard.type(text)
300 console.log('[48co] Typed', text.length, 'chars into focused app')
301 return true
302 } catch (err) {
303 console.error('[48co] nut-tree failed:', err.message)
304 // Fall through to clipboard fallback
277305 }
306 }
278307
279 console.log('[48co] Typed', text.length, 'chars into focused app')
308 // Method 2: Clipboard fallback (always works, but user needs to Ctrl+V)
309 try {
310 clipboard.writeText(text)
311 tray?.setToolTip('48co — Text copied! Press Ctrl+V to paste')
312 setTimeout(() => tray?.setToolTip('48co — Voice to Text'), 5000)
313 console.log('[48co] Copied', text.length, 'chars to clipboard (fallback)')
280314 return true
281 } catch (err) {
282 console.error('[48co] Keyboard simulation failed:', err.message)
283
284 // Fallback: copy to clipboard and tell user
285 try {
286 const { clipboard } = require('electron')
287 clipboard.writeText(text)
288 // Don't show a dialog — just notify via tray
289 tray?.setToolTip('48co — Text copied to clipboard (Ctrl+V to paste)')
290 setTimeout(() => tray?.setToolTip('48co — Voice to Text'), 5000)
291 } catch (clipErr) {
292 console.error('[48co] Clipboard fallback failed:', clipErr.message)
293 }
294
315 } catch (clipErr) {
316 console.error('[48co] Clipboard fallback failed:', clipErr.message)
295317 return false
296318 }
297319}
298320
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts