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 { 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();
|
||||||
|
|
|
||||||
|
|
@ -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