Skip to content

Tray and Window Lifecycle

Timon Home edited this page Mar 19, 2026 · 1 revision

Tray & Window Lifecycle


Window Creation

The main window is created in createWindow() in src/main/main.ts:

mainWindow = new BrowserWindow({
  width: 1200, height: 760, minWidth: 900, minHeight: 600,
  frame: false,
  backgroundColor: '#08090d',
  icon: getIconImage(),
  show: false,   // shown in ready-to-show to avoid white flash
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: false,
  },
})

frame: false removes the OS title bar entirely. TitleBar.tsx renders a custom replacement with drag region and window controls.

Show logic

mainWindow.once('ready-to-show', () => {
  if (isHidden || (settings.startMinimized && !IS_DEV))
    mainWindow?.hide()
  else
    mainWindow?.show()
})

isHidden is true when the app is launched with --hidden (e.g. via autostart). In dev mode startMinimized is always ignored so the window appears immediately.


Frameless Window Dragging

TitleBar.tsx applies CSS regions via inline style:

// Draggable region — the whole title bar
style={{ WebkitAppRegion: 'drag' }}

// Control buttons — must be non-draggable
style={{ WebkitAppRegion: 'no-drag' }}

Without the no-drag override, clicks on buttons are intercepted by the drag handler and never fire.


Close Behaviour

The close event on the window is intercepted:

mainWindow.on('close', (e) => {
  if (forceQuit) return
  if (getSettings().minimizeToTray) {
    e.preventDefault()
    mainWindow?.hide()
  }
})
  • If minimizeToTray is enabled, the window is hidden and the app stays alive
  • If disabled, the window closes and the app quits
  • forceQuit is set to true before any deliberate app.quit() call so the handler does not suppress it

The window:close IPC handler follows the same logic from the custom close button in TitleBar.


System Tray

The tray is created once in createTray() and never destroyed while the app is running.

Icon

getIconImage() tries icon.ico then icon.png from the resources/ directory. In dev, resources are resolved relative to the project root. In production, they are inside process.resourcesPath. Falls back to a 1×1 transparent PNG if neither file exists.

Tray icon is resized to 16×16 for display in the notification area:

tray = new Tray(getIconImage().resize({ width: 16, height: 16 }))

Tray menu

updateTrayMenu() rebuilds the context menu whenever process state changes:

Open Java Runner Client
─────────────────────
  ProfileName  (PID 12345)   ← one entry per running process
  ...
─────────────────────
Quit

When no processes are running, a disabled "No processes running" item is shown instead.

Tray interactions

Interaction Behaviour
Double-click mainWindow.show() + focus()
Right-click Shows context menu
"Open" item mainWindow.show() + focus()
"Quit" item Sets forceQuit = true, calls app.quit()

App Lifecycle Hooks

app.on('window-all-closed', () => { /* intentionally empty — keep alive in tray */ })
app.on('before-quit',       () => { forceQuit = true })
app.on('activate',          () => { mainWindow?.show() })  // macOS dock click

Processes spawned by JRC are children of the main process. They are not automatically killed when JRC quits — this is by design so JARs can outlive the manager UI. Use the Stop button or Utilities → Process Scanner to terminate them explicitly.


Auto-Start Registration

The launchOnStartup setting is stored and exposed in the settings UI, but OS-level registration via app.setLoginItemSettings() is not yet wired in main.ts. This is a planned implementation gap. When implemented, the setting should call:

app.setLoginItemSettings({
  openAtLogin: settings.launchOnStartup,
  args: settings.startMinimized ? ['--hidden'] : [],
})

Clone this wiki locally