feat(desktop): native menubar bridge + best-effort file logger

Two new modules under the existing desktop/ subtree, both no-op
outside a Tauri runtime (tauriBridge.listen / .invoke fail gracefully).

  desktop/menu.ts — listens for the `velxio://menu` event the Rust
    shell emits from the native menubar (Velxio Desktop / File / Edit
    / View / Help, full menu defined in
    pro/desktop/src-tauri/src/menu.rs). Internal actions handled
    directly here: Save .vlx and Open .vlx via utils/vlxFile, Toggle
    Serial Monitor via useSimulatorStore, Check for Updates via the
    tauri-plugin-updater global. The rest (new-project, Find,
    Toggle File Explorer) re-emit as window CustomEvent so the owners
    of that UI state can subscribe without pulling this module in.

  desktop/log.ts — `dlog(message, extra?)` round-trips a line to a
    Rust `write_debug_log` command that appends to
    `<app_data_dir>/desktop-debug.log`. Packaged Tauri apps have no
    devtools or stdout capture, so this is the only way to see what
    the webview did when a user reports a bug. Falls back to plain
    console.log when the command isn't registered (older shell).

mountDesktop() now installs the menu listener and dlog's its own
start — useful as a "did the desktop module even load" smoke marker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
davidmonterocrespo24 2026-05-22 18:08:46 -03:00
parent 54adac3ae3
commit 30d0da430b
3 changed files with 164 additions and 2 deletions

View File

@ -22,6 +22,8 @@ import { DesktopWelcomePage } from './DesktopWelcomePage';
import { Esp32QemuPrompt } from './Esp32QemuPrompt'; import { Esp32QemuPrompt } from './Esp32QemuPrompt';
import { GraceBanner } from './GraceBanner'; import { GraceBanner } from './GraceBanner';
import { invoke, isTauri, type ValidationResult } from './tauriBridge'; import { invoke, isTauri, type ValidationResult } from './tauriBridge';
import { installDesktopMenuListener } from './menu';
import { dlog } from './log';
import './desktop.css'; import './desktop.css';
let mounted = false; let mounted = false;
@ -101,8 +103,12 @@ async function checkInitialLicense(): Promise<void> {
export const mountDesktop = (): void => { export const mountDesktop = (): void => {
if (mounted) return; if (mounted) return;
mounted = true; mounted = true;
// eslint-disable-next-line no-console dlog('mountDesktop — Tauri shell active');
console.info('[desktop] 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(); mountSidePanels();
void checkInitialLicense(); void checkInitialLicense();

View File

@ -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
* `<app_data_dir>/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<void>('write_debug_log', { message: line }).catch(() => {
/* logging must never break the app */
});
}

View File

@ -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: '<id>' }`. 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:<action>`:
* - 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<void> {
if (installed) return;
installed = true;
await listen<MenuEventPayload>('velxio://menu', (event) => {
dlog('menu event', event.payload);
void handle(event.payload.action);
});
}
async function handle(action: MenuAction): Promise<void> {
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<void> {
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}`);
}
}