diff --git a/frontend/src/desktop/index.ts b/frontend/src/desktop/index.ts index 458ab23c..f17ac00c 100644 --- a/frontend/src/desktop/index.ts +++ b/frontend/src/desktop/index.ts @@ -22,6 +22,8 @@ import { DesktopWelcomePage } from './DesktopWelcomePage'; import { Esp32QemuPrompt } from './Esp32QemuPrompt'; import { GraceBanner } from './GraceBanner'; import { invoke, isTauri, type ValidationResult } from './tauriBridge'; +import { installDesktopMenuListener } from './menu'; +import { dlog } from './log'; import './desktop.css'; let mounted = false; @@ -101,8 +103,12 @@ async function checkInitialLicense(): Promise { export const mountDesktop = (): void => { if (mounted) return; mounted = true; - // eslint-disable-next-line no-console - console.info('[desktop] mountDesktop() — Tauri shell active'); + dlog('mountDesktop — Tauri shell active'); + + // Native menubar (Velxio / File / Edit / View / Help) sends events + // here. Hook the listener before any UI is mounted so the first + // user click is never dropped. + void installDesktopMenuListener(); mountSidePanels(); void checkInitialLicense(); diff --git a/frontend/src/desktop/log.ts b/frontend/src/desktop/log.ts new file mode 100644 index 00000000..57d9528a --- /dev/null +++ b/frontend/src/desktop/log.ts @@ -0,0 +1,39 @@ +/** + * Best-effort file logger for Tauri desktop builds. + * + * Packaged Tauri apps have no devtools and no stdout capture, so + * `console.log` vanishes. This helper round-trips messages to a Rust + * command (`write_debug_log`) that appends them to + * `/desktop-debug.log` — on Windows that's + * `%APPDATA%\dev.velxio.desktop\desktop-debug.log`. The user (or + * support) can open the file to see what the webview was doing. + * + * The Rust command lives in the velxio-prod overlay, not upstream + * — the OSS Tauri shell wires it through `pro/desktop/src-tauri/src/ + * lib.rs::write_debug_log`. If the command isn't registered (running + * an older shell), the log call silently no-ops; we still print to + * console so devtools / `tauri dev` keep working. + */ + +import { invoke, isTauri } from './tauriBridge'; + +const PREFIX = '[velxio-desktop]'; + +export function dlog(message: string, extra?: unknown): void { + // Always echo to console — `tauri dev` (or browser-loaded dev mode) + // can see this even without the Rust-side file. + // eslint-disable-next-line no-console + console.log(PREFIX, message, extra ?? ''); + if (!isTauri()) return; + let line = message; + if (extra !== undefined) { + try { + line += ' ' + JSON.stringify(extra); + } catch { + line += ' ' + String(extra); + } + } + invoke('write_debug_log', { message: line }).catch(() => { + /* logging must never break the app */ + }); +} diff --git a/frontend/src/desktop/menu.ts b/frontend/src/desktop/menu.ts new file mode 100644 index 00000000..3586737e --- /dev/null +++ b/frontend/src/desktop/menu.ts @@ -0,0 +1,117 @@ +/** + * Native menubar event bridge. + * + * The Tauri shell (pro/desktop/src-tauri/src/menu.rs in velxio-prod) + * builds a Velxio / File / Edit / View / Help menubar. Internal items + * (Save .vlx, Open .vlx, Toggle Serial Monitor, Find, …) emit a + * `velxio://menu` event with `{ action: '' }`. URL items (Docs, + * Examples, Discord, GitHub) are opened directly from Rust and don't + * reach this listener. + * + * Actions handled directly here (no further plumbing needed): + * - save-vlx, open-vlx → triggerDownloadVlx / file picker + * - toggle-serial-monitor → useSimulatorStore.toggleSerialMonitor() + * - check-for-updates → tauri-plugin-updater check() + * + * Actions forwarded to whoever's listening as a window CustomEvent + * `velxio:menu:`: + * - new-project, find-in-editor, toggle-file-explorer + * + * No-op outside Tauri (e.g. running the bundle in a regular browser + * for debugging) — listen() returns a no-op when the global event + * API isn't present. + */ + +import { listen } from './tauriBridge'; +import { dlog } from './log'; +import { triggerDownloadVlx, importVlxFile } from '../utils/vlxFile'; +import { useSimulatorStore } from '../store/useSimulatorStore'; + +type MenuAction = + | 'new-project' + | 'save-vlx' + | 'open-vlx' + | 'find-in-editor' + | 'toggle-file-explorer' + | 'toggle-serial-monitor' + | 'check-for-updates'; + +interface MenuEventPayload { + action: MenuAction; +} + +let installed = false; + +export async function installDesktopMenuListener(): Promise { + if (installed) return; + installed = true; + await listen('velxio://menu', (event) => { + dlog('menu event', event.payload); + void handle(event.payload.action); + }); +} + +async function handle(action: MenuAction): Promise { + switch (action) { + case 'save-vlx': + triggerDownloadVlx(); + return; + case 'open-vlx': + pickAndImportVlx(); + return; + case 'toggle-serial-monitor': + useSimulatorStore.getState().toggleSerialMonitor(); + return; + case 'new-project': + case 'find-in-editor': + case 'toggle-file-explorer': + window.dispatchEvent(new CustomEvent(`velxio:menu:${action}`)); + return; + case 'check-for-updates': + await checkForUpdates(); + return; + } +} + +function pickAndImportVlx(): void { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.vlx,application/json'; + input.style.display = 'none'; + document.body.appendChild(input); + input.addEventListener('change', async () => { + const file = input.files?.[0]; + if (file) { + try { + await importVlxFile(file); + } catch (err) { + // eslint-disable-next-line no-alert + alert(`Failed to open .vlx: ${(err as Error).message}`); + } + } + document.body.removeChild(input); + }); + input.click(); +} + +async function checkForUpdates(): Promise { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const updater = (window as any).__TAURI__?.updater; + if (!updater?.check) { + // eslint-disable-next-line no-alert + alert('Update plugin not available in this build.'); + return; + } + const update = await updater.check(); + if (update) { + await update.downloadAndInstall(); + } else { + // eslint-disable-next-line no-alert + alert('Velxio Desktop is up to date.'); + } + } catch (err) { + // eslint-disable-next-line no-alert + alert(`Update check failed: ${(err as Error).message}`); + } +}