From fac4ba3cde357234b45f7875dff2f70def1b7bd5 Mon Sep 17 00:00:00 2001 From: Avi Date: Wed, 30 Sep 2026 09:35:25 -0500 Subject: [PATCH] feat(desktop): launch from the applications list (folio-style) - launch-keynctr.sh: builds renderer/electron main if missing, exports KEYNCTR_ENABLE_GPU=1 (software rendering dies 'GPU process isn't usable' on this Hyprland box), execs bundled electron with --class=keynectr for WM_CLASS grouping. - keynctr.desktop installed at ~/.local/share/applications/ (validated, icon = public/icon.png, categories Utility, StartupWMClass keynectr). - requestSingleInstanceLock: second launch focuses the existing window (app.exit(0) in the doomed instance) instead of a duplicate shell fighting the vault. Verified via gtk-launch: 'keynectr | Keynctr' window maps; second gtk-launch keeps exactly 1 window. --- frontend/electron/main.ts | 18 ++++++++++++++++++ launch-keynctr.sh | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100755 launch-keynctr.sh diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 9af5e94..589dc87 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -1002,6 +1002,24 @@ if (process.platform === 'linux') { app.setDesktopName('keynectr.desktop'); } +// Launched from the applications list a second time? Focus the existing +// window instead of starting a duplicate app (two Electron shells would +// each spawn their own backend and fight over the vault file). +const gotInstanceLock = app.requestSingleInstanceLock(); +if (!gotInstanceLock) { + // Hard exit: app.quit() is async and would let the rest of this module + // (whenReady → backend spawn) run in the doomed second instance. + app.exit(0); +} else { + app.on('second-instance', () => { + const [window] = BrowserWindow.getAllWindows(); + if (window) { + if (window.isMinimized()) window.restore(); + window.focus(); + } + }); +} + app.whenReady().then(() => { protocol.handle('app', (request) => { const { pathname } = new URL(request.url); diff --git a/launch-keynctr.sh b/launch-keynctr.sh new file mode 100755 index 0000000..9ec7b6a --- /dev/null +++ b/launch-keynctr.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Launch Keynctr desktop (Electron shell + Rust backend spawned on demand). +# Production mode: loads the built renderer (frontend/dist) — no Vite needed. +set -euo pipefail + +APP_DIR="/home/avi/Projects/Keynctr/frontend" +cd "$APP_DIR" + +# The Electron shell spawns target/release/keynectr; make sure it exists and +# is newer-checked lazily by the app itself. We only guarantee the renderer +# bundle is present, else build it (first run or after a clean checkout). +if [[ ! -f "$APP_DIR/dist/index.html" || ! -f "$APP_DIR/dist-electron/main.js" ]]; then + npm run build >/dev/null 2>&1 || npm run build + npm run electron:build >/dev/null 2>&1 || npm run electron:build +fi + +# On this Hyprland machine native-Wayland rendering needs hardware GL: +# software rendering dies with "GPU process isn't usable" (verified +# 2026-09-30). Same setting the dev workflow uses. +export KEYNCTR_ENABLE_GPU=1 + +# --class pins WM_CLASS so the launcher icon groups with the window +# (StartupWMClass=keynectr in the .desktop file). +exec "$APP_DIR/node_modules/.bin/electron" --class=keynectr "$APP_DIR" "$@"