@@ -22,6 +22,7 @@ import { JsonPairingStore } from './pairing/pairing-store';
2222import { PairingManager } from './pairing/pairing-manager' ;
2323import { registerServerIpc } from './server-ipc' ;
2424import { registerSettingsWindowIpc } from './settings-window-ipc' ;
25+ import { secondInstanceAction } from './single-instance' ;
2526import { registerSystemStartupIpc } from './system-startup-ipc' ;
2627import { shouldStartHidden , SystemStartupService } from './system-startup' ;
2728import { createSwitchifyTray , type SwitchifyTray } from './tray' ;
@@ -45,6 +46,8 @@ if (process.platform === 'win32' && app.isPackaged) {
4546 app . commandLine . appendSwitch ( 'disable-gpu-sandbox' ) ;
4647}
4748
49+ const gotSingleInstanceLock = app . requestSingleInstanceLock ( ) ;
50+
4851// Matches --color-bg in src/renderer/styles.css so the window frame paints
4952// the correct base before the renderer loads.
5053function shellBackgroundColor ( ) : string {
@@ -233,139 +236,151 @@ function quitApp(): void {
233236 app . quit ( ) ;
234237}
235238
236- app . whenReady ( ) . then ( ( ) => {
237- if ( process . platform === 'win32' ) {
238- app . setAppUserModelId ( WINDOWS_APP_USER_MODEL_ID ) ;
239- }
239+ if ( ! gotSingleInstanceLock ) {
240+ app . quit ( ) ;
241+ } else {
242+ app . on ( 'second-instance' , ( _event , argv ) => {
243+ if ( secondInstanceAction ( argv , process . platform ) === 'ignore' ) {
244+ return ;
245+ }
240246
241- const startHidden = shouldStartHidden ( process . argv , process . platform ) ;
242- const systemStartup = new SystemStartupService ( {
243- platform : process . platform ,
244- isPackaged : app . isPackaged ,
245- executablePath : process . execPath ,
246- appUserModelId : WINDOWS_APP_USER_MODEL_ID ,
247- getLoginItemSettings : ( options ) => app . getLoginItemSettings ( options ) ,
248- setLoginItemSettings : ( settings ) => app . setLoginItemSettings ( settings )
247+ showMainWindow ( ) ;
249248 } ) ;
250- const pairingStore = new JsonPairingStore ( join ( app . getPath ( 'userData' ) , 'pairing-state.json' ) ) ;
251- const cursorOverlaySettingsStore = new JsonCursorOverlaySettingsStore (
252- join ( app . getPath ( 'userData' ) , 'cursor-overlay-settings.json' )
253- ) ;
254- const pairingManager = new PairingManager ( pairingStore ) ;
255- const pairingApprovalManager = new PairingApprovalManager ( pairingStore ) ;
256- const inputAdapter = new LibnutWin32InputAdapter ( ( position ) => screen . getDisplayNearestPoint ( position ) . scaleFactor ) ;
257- cursorOverlay = new CursorOverlay ( { settings : cursorOverlaySettingsStore . load ( ) } ) ;
258- const commandExecutor = new DesktopCommandExecutor ( inputAdapter , cursorOverlay ) ;
259- releaseHeldMouseButtons = ( ) => commandExecutor . releaseHeldMouseButtons ( ) ;
260- const releaseHeldMouseButtonsSafely = ( ) : void => {
261- void commandExecutor . releaseHeldMouseButtons ( ) . catch ( ( error ) => {
262- console . warn ( error instanceof Error ? error . message : 'Could not release held mouse buttons.' ) ;
249+
250+ app . whenReady ( ) . then ( ( ) => {
251+ if ( process . platform === 'win32' ) {
252+ app . setAppUserModelId ( WINDOWS_APP_USER_MODEL_ID ) ;
253+ }
254+
255+ const startHidden = shouldStartHidden ( process . argv , process . platform ) ;
256+ const systemStartup = new SystemStartupService ( {
257+ platform : process . platform ,
258+ isPackaged : app . isPackaged ,
259+ executablePath : process . execPath ,
260+ appUserModelId : WINDOWS_APP_USER_MODEL_ID ,
261+ getLoginItemSettings : ( options ) => app . getLoginItemSettings ( options ) ,
262+ setLoginItemSettings : ( settings ) => app . setLoginItemSettings ( settings )
263263 } ) ;
264- } ;
265- controlService = new ControlService ( {
266- pairingManager,
267- pairingApprovalManager,
268- authValidator : new CommandAuthValidator ( pairingStore ) ,
269- getPointerProfile : ( ) => {
270- const cursor = inputAdapter . getMousePosition ( ) ;
271- const display = screen . getDisplayNearestPoint ( cursor ) ;
272- return createPointerMovementProfile ( {
273- cursor,
274- display : {
275- bounds : display . bounds ,
276- scaleFactor : display . scaleFactor
277- }
264+ const pairingStore = new JsonPairingStore ( join ( app . getPath ( 'userData' ) , 'pairing-state.json' ) ) ;
265+ const cursorOverlaySettingsStore = new JsonCursorOverlaySettingsStore (
266+ join ( app . getPath ( 'userData' ) , 'cursor-overlay-settings.json' )
267+ ) ;
268+ const pairingManager = new PairingManager ( pairingStore ) ;
269+ const pairingApprovalManager = new PairingApprovalManager ( pairingStore ) ;
270+ const inputAdapter = new LibnutWin32InputAdapter ( ( position ) => screen . getDisplayNearestPoint ( position ) . scaleFactor ) ;
271+ cursorOverlay = new CursorOverlay ( { settings : cursorOverlaySettingsStore . load ( ) } ) ;
272+ const commandExecutor = new DesktopCommandExecutor ( inputAdapter , cursorOverlay ) ;
273+ releaseHeldMouseButtons = ( ) => commandExecutor . releaseHeldMouseButtons ( ) ;
274+ const releaseHeldMouseButtonsSafely = ( ) : void => {
275+ void commandExecutor . releaseHeldMouseButtons ( ) . catch ( ( error ) => {
276+ console . warn ( error instanceof Error ? error . message : 'Could not release held mouse buttons.' ) ;
278277 } ) ;
279- } ,
280- onStatusChange : ( status ) => {
281- if ( status . connectedClientCount === 0 ) {
282- releaseHeldMouseButtonsSafely ( ) ;
283- cursorOverlay ?. endControlSession ( ) ;
278+ } ;
279+ controlService = new ControlService ( {
280+ pairingManager,
281+ pairingApprovalManager,
282+ authValidator : new CommandAuthValidator ( pairingStore ) ,
283+ getPointerProfile : ( ) => {
284+ const cursor = inputAdapter . getMousePosition ( ) ;
285+ const display = screen . getDisplayNearestPoint ( cursor ) ;
286+ return createPointerMovementProfile ( {
287+ cursor,
288+ display : {
289+ bounds : display . bounds ,
290+ scaleFactor : display . scaleFactor
291+ }
292+ } ) ;
293+ } ,
294+ onStatusChange : ( status ) => {
295+ if ( status . connectedClientCount === 0 ) {
296+ releaseHeldMouseButtonsSafely ( ) ;
297+ cursorOverlay ?. endControlSession ( ) ;
298+ }
299+ tray ?. update ( ) ;
300+ } ,
301+ onClientDisconnecting : ( connectionId ) => {
302+ bluetoothTransport ?. markClientRequestedDisconnect ( connectionId ) ;
303+ } ,
304+ onCommand : ( command ) => commandExecutor . execute ( command )
305+ } ) ;
306+ void pairingManager . getDesktopId ( ) . then ( ( desktopId ) => {
307+ controlService ?. setDesktopId ( desktopId ) ;
308+ } ) ;
309+ bluetoothTransport = new WindowsBluetoothTransport ( {
310+ controlService,
311+ getDesktopId : ( ) => pairingManager . getDesktopId ( ) ,
312+ displayName : 'Switchify PC' ,
313+ onStatusChange : ( ) => {
314+ tray ?. update ( ) ;
284315 }
285- tray ?. update ( ) ;
286- } ,
287- onClientDisconnecting : ( connectionId ) => {
288- bluetoothTransport ?. markClientRequestedDisconnect ( connectionId ) ;
289- } ,
290- onCommand : ( command ) => commandExecutor . execute ( command )
291- } ) ;
292- void pairingManager . getDesktopId ( ) . then ( ( desktopId ) => {
293- controlService ?. setDesktopId ( desktopId ) ;
294- } ) ;
295- bluetoothTransport = new WindowsBluetoothTransport ( {
296- controlService,
297- getDesktopId : ( ) => pairingManager . getDesktopId ( ) ,
298- displayName : 'Switchify PC' ,
299- onStatusChange : ( ) => {
300- tray ?. update ( ) ;
301- }
302- } ) ;
303- registerServerIpc ( controlService , pairingStore ) ;
304- registerCursorOverlayIpc ( cursorOverlay , cursorOverlaySettingsStore ) ;
305- registerPairingApprovalIpc ( controlService ) ;
306- registerSettingsWindowIpc ( showSettingsWindow ) ;
307- registerAppWindowIpc ( ) ;
308- registerExternalUrlIpc ( ) ;
309- registerSystemStartupIpc ( systemStartup ) ;
310- registerUpdateIpc (
311- new UpdateService ( {
312- currentVersion : app . getVersion ( ) ,
313- isPackaged : app . isPackaged ,
314- platform : process . platform ,
315- autoUpdater
316- } )
317- ) ;
318- void bluetoothTransport . start ( ) ;
319-
320- mainWindow = createMainWindow ( { showOnReady : ! startHidden } ) ;
321- tray = createSwitchifyTray ( {
322- getStatus : ( ) =>
323- controlService ?. getStatus ( ) ?? {
324- state : 'stopped' ,
325- desktopId : null ,
326- connectedClientCount : 0 ,
327- connectedClients : [ ] ,
328- lastSeenAt : null ,
329- lastError : null ,
330- bluetooth : DEFAULT_BLUETOOTH_STATUS
316+ } ) ;
317+ registerServerIpc ( controlService , pairingStore ) ;
318+ registerCursorOverlayIpc ( cursorOverlay , cursorOverlaySettingsStore ) ;
319+ registerPairingApprovalIpc ( controlService ) ;
320+ registerSettingsWindowIpc ( showSettingsWindow ) ;
321+ registerAppWindowIpc ( ) ;
322+ registerExternalUrlIpc ( ) ;
323+ registerSystemStartupIpc ( systemStartup ) ;
324+ registerUpdateIpc (
325+ new UpdateService ( {
326+ currentVersion : app . getVersion ( ) ,
327+ isPackaged : app . isPackaged ,
328+ platform : process . platform ,
329+ autoUpdater
330+ } )
331+ ) ;
332+ void bluetoothTransport . start ( ) ;
333+
334+ mainWindow = createMainWindow ( { showOnReady : ! startHidden } ) ;
335+ tray = createSwitchifyTray ( {
336+ getStatus : ( ) =>
337+ controlService ?. getStatus ( ) ?? {
338+ state : 'stopped' ,
339+ desktopId : null ,
340+ connectedClientCount : 0 ,
341+ connectedClients : [ ] ,
342+ lastSeenAt : null ,
343+ lastError : null ,
344+ bluetooth : DEFAULT_BLUETOOTH_STATUS
345+ } ,
346+ showWindow : showMainWindow ,
347+ openSettings : showSettingsWindow ,
348+ disconnectClients : ( ) => {
349+ releaseHeldMouseButtonsSafely ( ) ;
350+ controlService ?. disconnectClients ( ) ;
351+ tray ?. update ( ) ;
331352 } ,
332- showWindow : showMainWindow ,
333- openSettings : showSettingsWindow ,
334- disconnectClients : ( ) => {
335- releaseHeldMouseButtonsSafely ( ) ;
336- controlService ?. disconnectClients ( ) ;
337- tray ?. update ( ) ;
338- } ,
339- quit : quitApp
353+ quit : quitApp
354+ } ) ;
355+
356+ app . on ( 'activate' , ( ) => {
357+ showMainWindow ( ) ;
358+ } ) ;
340359 } ) ;
341360
342- app . on ( 'activate' , ( ) => {
343- showMainWindow ( ) ;
361+ app . on ( 'window-all-closed' , ( ) => { } ) ;
362+
363+ app . on ( 'before-quit' , ( event ) => {
364+ if ( isQuitting ) return ;
365+
366+ event . preventDefault ( ) ;
367+ isQuitting = true ;
368+ void ( releaseHeldMouseButtons ?.( ) ?? Promise . resolve ( ) )
369+ . catch ( ( error ) => {
370+ console . warn ( error instanceof Error ? error . message : 'Could not release held mouse buttons.' ) ;
371+ } )
372+ . then ( ( ) => {
373+ tray ?. destroy ( ) ;
374+ tray = null ;
375+ cursorOverlay ?. destroy ( ) ;
376+ cursorOverlay = null ;
377+ bluetoothTransport ?. stop ( ) ;
378+ bluetoothTransport = null ;
379+ releaseHeldMouseButtons = null ;
380+ controlService = null ;
381+ } )
382+ . finally ( ( ) => {
383+ app . quit ( ) ;
384+ } ) ;
344385 } ) ;
345- } ) ;
346-
347- app . on ( 'window-all-closed' , ( ) => { } ) ;
348-
349- app . on ( 'before-quit' , ( event ) => {
350- if ( isQuitting ) return ;
351-
352- event . preventDefault ( ) ;
353- isQuitting = true ;
354- void ( releaseHeldMouseButtons ?.( ) ?? Promise . resolve ( ) )
355- . catch ( ( error ) => {
356- console . warn ( error instanceof Error ? error . message : 'Could not release held mouse buttons.' ) ;
357- } )
358- . then ( ( ) => {
359- tray ?. destroy ( ) ;
360- tray = null ;
361- cursorOverlay ?. destroy ( ) ;
362- cursorOverlay = null ;
363- bluetoothTransport ?. stop ( ) ;
364- bluetoothTransport = null ;
365- releaseHeldMouseButtons = null ;
366- controlService = null ;
367- } )
368- . finally ( ( ) => {
369- app . quit ( ) ;
370- } ) ;
371- } ) ;
386+ }
0 commit comments