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:
parent
54adac3ae3
commit
30d0da430b
|
|
@ -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<void> {
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
});
|
||||
}
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue