Claude/unified design #4006
1 changed file+43−37
Modifieddesktop/main/index.js+43−37View fileUnifiedSplit
@@ -41,7 +41,7 @@ const store = new Store({
4141 vocabulary: [], // [{from, to}]
4242 replacements: [], // [{from, to}]
4343 launchAtLogin: true,
44 showOverlay: true,
44 showOverlay: false, // OFF by default — no UI on screen, just types
4545 typingSpeed: 0, // 0 = instant (paste), >0 = ms between chars
4646 aiRewrite: false, // AI rewrite mode — polishes dictated text
4747 aiRewriteMode: 'professional', // professional | casual | concise | auto
@@ -201,19 +201,19 @@ async function startRecording() {
201201
202202 updateTrayMenu()
203203
204 // Show overlay
205 if (store.get('showOverlay') && overlayWindow) {
206 overlayWindow.webContents.send('recording-state', { status: 'recording' })
207 overlayWindow.showInactive() // showInactive = don't steal focus
208 }
209
210 // Tell renderer to start capturing audio
204 // Tell renderer to start capturing audio (overlay handles mic access)
211205 if (overlayWindow) {
212206 overlayWindow.webContents.send('start-recording', {
213207 engine: store.get('engine'),
214208 language: store.get('language'),
215209 whisperApiKey: store.get('whisperApiKey'),
216210 })
211 overlayWindow.webContents.send('recording-state', { status: 'recording' })
212
213 // Only show overlay if user wants it — default is OFF (zero UI)
214 if (store.get('showOverlay')) {
215 overlayWindow.showInactive()
216 }
217217 }
218218}
219219
@@ -233,58 +233,63 @@ async function stopRecording() {
233233// ─── Keyboard Simulation (type into focused app) ──────────────
234234async function typeText(text) {
235235 try {
236 const { keyboard } = require('@nut-tree-fork/nut-js')
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))
237246
238247 const speed = store.get('typingSpeed', 0)
239248
240249 if (speed === 0) {
241 // Instant mode: use clipboard paste (fastest, most reliable)
242 const { clipboard } = require('electron')
250 // Instant mode: clipboard paste (fastest, most reliable)
243251 const originalClipboard = clipboard.readText()
244
245252 clipboard.writeText(text)
246253
247 // Small delay to ensure clipboard is ready
248 await new Promise(r => setTimeout(r, 50))
254 // Ensure clipboard is ready
255 await new Promise(r => setTimeout(r, 100))
249256
250257 // Simulate Ctrl+V / Cmd+V
251258 if (process.platform === 'darwin') {
252 await keyboard.pressKey(require('@nut-tree-fork/nut-js').Key.LeftSuper)
253 await keyboard.pressKey(require('@nut-tree-fork/nut-js').Key.V)
254 await keyboard.releaseKey(require('@nut-tree-fork/nut-js').Key.V)
255 await keyboard.releaseKey(require('@nut-tree-fork/nut-js').Key.LeftSuper)
259 await keyboard.pressKey(Key.LeftSuper)
260 await keyboard.pressKey(Key.V)
261 await keyboard.releaseKey(Key.V)
262 await keyboard.releaseKey(Key.LeftSuper)
256263 } else {
257 await keyboard.pressKey(require('@nut-tree-fork/nut-js').Key.LeftControl)
258 await keyboard.pressKey(require('@nut-tree-fork/nut-js').Key.V)
259 await keyboard.releaseKey(require('@nut-tree-fork/nut-js').Key.V)
260 await keyboard.releaseKey(require('@nut-tree-fork/nut-js').Key.LeftControl)
264 await keyboard.pressKey(Key.LeftControl)
265 await keyboard.pressKey(Key.V)
266 await keyboard.releaseKey(Key.V)
267 await keyboard.releaseKey(Key.LeftControl)
261268 }
262269
263 // Restore original clipboard after a brief delay
270 // Restore original clipboard after paste completes
264271 setTimeout(() => {
265 clipboard.writeText(originalClipboard)
272 try { clipboard.writeText(originalClipboard) } catch {}
266273 }, 500)
267274 } else {
268275 // Character-by-character typing mode
269276 await keyboard.type(text)
270277 }
271278
279 console.log('[48co] Typed', text.length, 'chars into focused app')
272280 return true
273281 } catch (err) {
274 console.error('Keyboard simulation failed:', err)
282 console.error('[48co] Keyboard simulation failed:', err.message)
275283
276 // Fallback: try clipboard paste via Electron
284 // Fallback: copy to clipboard and tell user
277285 try {
278286 const { clipboard } = require('electron')
279287 clipboard.writeText(text)
280 dialog.showMessageBox({
281 type: 'info',
282 title: '48co',
283 message: 'Text copied to clipboard',
284 detail: 'Keyboard simulation failed. Text has been copied to your clipboard — press Ctrl+V / Cmd+V to paste.',
285 })
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)
286291 } catch (clipErr) {
287 console.error('Clipboard fallback also failed:', clipErr)
292 console.error('[48co] Clipboard fallback failed:', clipErr.message)
288293 }
289294
290295 return false
@@ -689,10 +694,11 @@ app.whenReady().then(async () => {
689694 console.error('Failed to register global shortcut:', shortcut)
690695 }
691696
692 // Push-to-talk: Ctrl+Shift hold
693 // Note: True push-to-talk with key hold detection requires platform-specific
694 // native modules. For now, we use the toggle shortcut. Push-to-talk can be
695 // added later with iohook or similar native key listener.
697 // Mouse wheel click listener (global)
698 // Electron doesn't have a native global mouse listener, so we use the
699 // overlay window + a transparent full-screen input catcher for mouse events.
700 // For now, users can configure hotkeys in settings.
701 // TODO: Add iohook or similar for true global mouse button detection
696702
697703 // Auto-update check
698704 if (app.isPackaged) {
699705
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts