Chapter 8 of 15

Being a real desktop app

Freddy wants a quick-note window behind a shortcut, a File menu, an icon in the menu bar, and an app that opens a note from a link. None of it is a web page's business, and all of it is a few builder calls.

Features first

Most of this chapter is behind Cargo features, so an app only compiles in what it uses. Freddy adds them to the same dependency line:

elyra = { git = "https://github.com/kwhorne/elyra-framework", tag = "v0.16.0",
          features = ["database", "tray", "shortcuts", "autostart", "system"] }

Windows, the menu, single-instance and deep links are core and need no feature. The framework runs on macOS, Windows and Linux; where a platform behaves differently, the guide says so, and so does this chapter.

Windows

The primary window is configured on App; more can be declared at startup or opened at runtime, and all of them share one event loop, one protocol handler and one event bus:

App::new()
    .title("Freddy Notes")
    .size(900.0, 640.0)
    .min_size(420.0, 480.0)
    .persist_window_state()      // size, position and maximized, restored next launch
    .run();

#[command]
async fn open_quick_note(ctx: Ctx) {
    ctx.get::<Windows>().open(
        WindowConfig::new("quick").title("Quick note").size(420.0, 260.0)
            .always_on_top(true).path("quick"),
    );
}

path opens a route of the Svelte app, so the quick-note window is just another page. From the page, appWindow in @elyra/runtime minimizes, sets the title, centers, closes and reports size and focus; onFileDrop hands over the absolute paths of files dropped on the window, which is how Freddy imports Markdown files. The app exits when the last window closes.

App::new().menu(
    Menu::new().submenu(
        Submenu::new("File")
            .item_accel("file.new", "New note", "CmdOrCtrl+N")
            .separator()
            .item("file.export", "Export…"),
    ),
)

A click emits elyra:menu with the item's id, and the page handles it with onMenu. On macOS the submenus join the application menu bar next to the built-in app and Edit menus, the Edit menu being what makes ⌘C and ⌘V reach the webview; on Windows and Linux they become a per-window menu bar with a Help → About item.

The tray and a global shortcut

App::new()
    .tray(TrayConfig::new().tooltip("Freddy Notes")
        .item("quick", "Quick note").separator().quit("Quit"))
    .global_shortcut("CmdOrCtrl+Shift+N")

A tray item's id arrives on the "tray" channel from chapter 4, and a global shortcut, which fires even when the app is not focused, arrives through onShortcut with its accelerator string. Freddy sends both to open_quick_note. A shortcut that is invalid or already taken is logged and skipped, and the others still register. The tray generates a plain icon, so no image asset is needed to start.

One instance, and links that open a note

App::new().single_instance().deep_link("freddynotes").run()?;

With single_instance, a second launch hands its command line to the first process and exits; the first raises its window and gets the payload on elyra:second-instance. The handshake between them proves a random per-install token with an HMAC instead of sending it, so another local process cannot inject a fake deep link. With deep_link, Freddy reads the URL the app was launched with through deepLink.initial() and later ones through onDeepLink, and opens the note named in freddynotes://…. Registering the scheme with the OS is the bundle's job: without deep_link = "freddynotes" under [bundle] in elyra.toml, the scheme never reaches the app, which chapter 15 sets up.

Starting at login

import { autostart } from "@elyra/runtime";
if (!(await autostart.isEnabled())) await autostart.enable();

That is a LaunchAgent on macOS, the registry's Run key on Windows and a .desktop entry on Linux. Freddy puts it behind a checkbox in his settings, since starting at login is the user's decision.

Dialogs, the clipboard and the OS

The system feature adds what almost every app needs, driven from the page with no Rust wiring: dialog.open and dialog.save for files and folders, clipboard, notify, paths() for the standard folders, and shell.openExternal. That last one is policy-gated, because "open with the default app" must not become "run this program": only http, https and mailto by default, no file: URLs, and nothing executable. A blocked call rejects with a message instead of doing nothing.

Two things want a bundled app. On macOS, notifications are only delivered from a bundled app with a bundle identifier, and autostart may not persist for a bare cargo run binary. If they seem not to work while Freddy develops, that is why; chapter 15 bundles the app.

The guides are Windows, Application menu, System tray, Global shortcuts, Single-instance & deep-linking, Autostart and System integration.

What you learned

  • Windows are configured on App or opened at runtime with Windows::open, each able to open a route of the Svelte app.
  • The menu, the tray and global shortcuts arrive on the page as events, and the platforms render menus differently.
  • single_instance and deep_link make one app that opens links, with an authenticated handshake.
  • The system feature gives dialogs, the clipboard, notifications and paths, and openExternal is policy-gated.
  • On macOS, notifications and autostart want a bundled app.
Next: in Chapter 9 Freddy stores settings and secrets, and puts work in the background.