Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture & processes

Proton combines a native MoonBit application with the Chromium browser engine. The application process manages windows, application state and operating-system features. Web pages provide the interface and invoke host operations through messages.

Components and responsibilities

ComponentResponsibility
Native MoonBit applicationRuns the application entry point, business logic and asynchronous tasks
ProtonProvides APIs for windows, lifecycle, browser contents and frontend/host communication
CEF (Chromium Embedded Framework)Embeds Chromium in the native application, providing browser creation, callbacks and process communication interfaces
ChromiumImplements the web platform, including rendering, JavaScript, networking and browser storage
Web frontendImplements the application interface with HTML, CSS and JavaScript

Applications use the public API in moonbit-community/proton. Proton's native bindings are compiled from source into the application; CEF/Chromium is loaded as a separate runtime and bundled for distribution. Pages use that bundled browser engine rather than an operating-system WebView.

The frontend can use ordinary JavaScript or JavaScript produced by the MoonBit compiler. Rabbita is an optional MoonBit UI library; proton_rabbita integrates commands and subscriptions with it. Neither is a required part of the Proton runtime.

Process model

The application executable runs MoonBit host code and hosts CEF's browser process. This process manages native windows and browser instances and coordinates Chromium subprocesses. Page scripts execute in renderer processes; GPU and utility processes provide graphics and other browser services.

Application process
  MoonBit application logic
       |
  Proton -- Native windows
       |
  CEF browser process
       | Chromium inter-process communication
       +-- Renderer processes: pages, JavaScript, workers
       +-- GPU process: graphics
       +-- Utility and other processes: browser services

cef_process is the helper executable CEF launches for subprocesses. CEF selects the role at launch, so the same helper can serve different subprocess roles. It does not execute the application's MoonBit entry point. The helper matches the application's Proton release and CEF runtime.

Chromium manages process allocation according to pages and runtime state. One renderer can host multiple tasks, such as a page and workers. Window counts, browser-instance counts, task counts and OS process counts therefore do not map one to one. Task resource metrics cannot simply be summed into application-wide usage.

Windows and browser contents

One Proton application runtime can manage multiple native windows. Each window owns its main browser contents and can host additional child browser views:

Application runtime
  +-- Window A
  |    +-- Main browser contents
  |    +-- Child browser view
  +-- Window B
       +-- Main browser contents

A window owns its title, position, dimensions and native decorations. Browser contents own documents, navigation, scripts and page events. A child view is an independent browser within the window's content area, with its own document and bounds. Navigating it does not navigate the main page; removing it does not close the entire window.

These objects have distinct lifetimes: a page can reload or its renderer can terminate while the native window remains alive. Closing a window also closes its hosted browser contents. See the application API for creation, closing and recovery contracts.

Execution and communication boundaries

Native UI objects are managed by their creating thread. Proton connects native event processing to the moonbitlang/async external event loop: native callbacks enqueue records and wake the scheduler, then MoonBit handles events and application callbacks. Asynchronous application tasks can yield execution, but expensive synchronous work still occupies the host event loop.

Renderers and the host have separate execution environments. Proton injects a bridge into pages, delivers frontend requests to registered host handlers and returns their results or errors. The host can also send event notifications. Payloads are serialized; shared type definitions do not create shared memory.

proton_contract describes typed commands and events, and proton_client supports MoonBit frontend calls. JavaScript frontends use the page bridge directly. Host command bindings and capabilities select which operations are available to which pages. See commands, events and capabilities for protocol, cancellation and authorization behavior.

Development and distribution

Development and distribution use the same native host and browser architecture. The sources of page content and runtime files change.

ScenarioPage sourceNative runtime source
Inline HTMLA string compiled into the applicationShared runtime and helper installed by setup
Frontend development serverDevelopment URL configured through the CLIShared runtime and helper installed by setup
Packaged applicationInline content or packaged static resources; URL entries may also use remote pagesRuntime and helper shipped in the artifact

A frontend development server serves page resources; it does not run host business logic. Opening the same URL in an ordinary browser provides no Proton native bridge. Packaging assembles the application executable, required page resources, CEF runtime and helper into a distributable artifact. Configuration documents file responsibilities and output paths; Command Line Interface documents packaging formats and requirements.

Environment requirements

The installation commands below target 0.4.0, which is not yet available on Mooncakes. See Release Notes for API migration details.

Proton 0.4.0 supports the following host build environments. Native builds and packages target the host operating system; cross-compilation is not provided by the Proton CLI.

PlatformArchitectureBuild requirements
macOSApple SiliconXcode Command Line Tools, clang
Windowsx64Visual Studio C++ Build Tools, MSVC and Windows SDK; developer terminal
Linuxx64C/C++ build tools, pkg-config, X11/RandR and GTK development libraries; graphical environment for execution

On Ubuntu 24.04, the dependency set includes build-essential, pkg-config, libx11-dev, libxrandr-dev, libgtk-3-dev, libwebkit2gtk-4.1-dev, libnotify-dev, libnss3, libgbm1 and libasound2t64. Package names and runtime compatibility vary by distribution.

NSIS is required only for NSIS output. Signing identities and notarization credentials are distribution requirements, not prerequisites for local development.

Tool commands

ToolRoleInstallation or inspection
MoonBitCompiler, package manager and runtime toolsOfficial installation; moon version
Proton CLIProject development and packagingmoon install moonbit-community/proton_cli@0.4.0; proton_cli --version
WarrenIsomorphic frontend dev server and buildmoon install moonbit-community/warren@0.4.5; warren --help
Node.js / npmWarren's JavaScript build toolingNode.js installation; node --version, npm --version

MoonBit's binary directory must be on PATH. Minimal applications with inline HTML do not require Warren or Node.js. moonx's deprecated native mode is not required by the documented frontend commands.

Runtime installation

proton_cli cef setup resolves the release's Chromium runtime and matching source-built helper. Its initial run needs network access and may take several minutes. Arbitrary CEF downloads do not satisfy the release contract.

LocationContent
~/.proton/store/Immutable shared CEF SDK/runtime installations
~/.proton/helpers/Helpers selected by platform and Proton version
PROTON_RUNTIME_STOREOptional absolute override for runtime storage

Runtime/helper files are not copied into project source. They are assembled into distributable applications during packaging. CLI and Proton module versions must agree. proton_cli doctor checks project configuration, tools, runtime and helper without changing them.

To create an application with these tools, follow the minimal application tutorial. The repository checkout below is needed only to run the examples in this book.

Source checkout and runtime

Examples use the repository workspace. Clone the release commit, install the required tools, and set up its runtime once:

git clone https://github.com/moonbit-community/proton.git
cd proton
git checkout bdb169302952db553deda6de015887c7a6a19831
moon update
proton_cli cef setup

After setup, run the command listed on the chosen example page from this repository root. These commands use the repository workspace, not a scaffolded application.

Application API reference

The root moonbit-community/proton package is the public application facade. Use the builder before run; use context and handle operations while the application is running. CLI project metadata does not replace runtime configuration.

AreaOwner / entryReference
Startup, readiness, quit, task cleanupApp, ApplicationContext, lifecycle hooksLifecycle
Window declarations, instances, child browsersApp, WindowManager, WindowHandle, ViewHandleWindows
Request/response communicationcontract, registrar, clientCommands
Frontend notifications and subscription lifetimeemitters, client subscriptionsEvents
Host operations and renderer authorizationApp.capability, extension-specific scopeCapabilities
Cookies, browser storage, proxySessionHandle, startup builderBrowser sessions
Signed application updatesApp.update_channel, PendingUpdateUpdates
Process ownership and activationsingle-instance builder, context methodsProcess control
Paths, files and build metadataproject configuration and application pathsConfiguration

For the complete set of methods, overloads, typed errors and defaults, use the versioned API index. This reference describes when to use each entry point, which object owns its state, and how it behaves during startup, cancellation and shutdown.

Application lifecycle

App configures an application before startup. It is created through the root Proton facade and executed through the MoonBit async runtime. Proton installs its native event-loop integration during initialization; application code does not install or poll a separate UI loop.

Entry sources

ConstructorEntry
html(title, html, ...)Inline HTML document
url(title, url, ...)URL
file(title, path, ...)Local HTML file
asset(title, path, ...)Managed application asset entry

Each returns an App builder. Shared options include width, height, debug mode and resizability. entry_html, entry_url, entry_file and entry_asset configure the entry on an existing builder.

Configuring an application

Configuration methods, including config, debug, add_window, and on_* hook registration, modify the existing App and return Unit. Use .. to retain the application value across configuration calls:

let app = @proton.html("Hello", "<h1>Hello</h1>")
  ..config(@proton.load_config())
  ..debug()

app.single_instance()
app.run()

A direct call such as app.single_instance() returns Unit. Constructors still return App; queries such as app.name() retain their result types. run() and run_or_abort() remain ordinary async calls.

To migrate an existing builder chain, change its configuration calls from . to ... Keep . for queries and the final run() / run_or_abort(). Configuration validation, registration order, and lifecycle behavior are unchanged.

Identity

@proton.load_config() returns the complete typed AppConfig and reports loading errors immediately. Read identifier, backend, frontend, and package_config directly from this value. Install it with app.config(config), which is mutually exclusive with app.identifier(...). The identifier is stable application identity, distinct from the window title, product name, and version.

app_path(), resource_dir() and is_packaged() describe the execution environment. Development paths and packaged resources have different locations; frontend URLs are not backend filesystem paths.

Hooks and owned state

HookMeaning
app_lifecycle(on_start, on_shutdown)Application-level state; the start result is passed to shutdown
window_lifecycle(on_ready, on_close)Per-window state; the ready result is passed to close
on_window_close_requestAsync allow/deny decision before window closure

Application and window contexts expose task groups and window management. Window contexts additionally expose their handle and event emitter. A window's state must not outlive the resources it refers to.

Running the application

Calling run() starts the configured application and awaits its lifecycle. It is async and raises AppRunError on failure. run_or_abort() instead reports a failure and aborts. Startup, task ownership and exit behavior are described below.

Startup order

Startup validates configuration, resolves identity and paths, initializes logging, and acquires the single-instance lock when configured. A secondary instance that forwards its activation returns without creating an application runtime.

For the primary instance, session creation precedes application start hooks. Successful start hooks establish the application state. Proton then opens declared startup windows that were not already opened by a start hook, waits for the required bridge initialization, and activates the initial windows through their ready hooks. Application readiness follows successful initial activation; configuring a window or receiving a native creation event does not establish readiness.

A start hook may await context.windows().open(id). A committed exit stops startup. A denied quit request leaves the application running; requesting quit is not itself a committed exit.

Task and cleanup ownership

Use application task groups for work that belongs to the application, and window task groups for work that belongs to a window. Their scopes are canceled and drained on shutdown; detached work outside these scopes is not made lifecycle-safe by holding a window handle.

Each lifecycle hook that successfully returns state installs its paired cleanup hook. A hook that fails before returning has not produced state to pass to its paired cleanup; previously established scopes still clean up. Cleanup hooks run under cancellation protection. Reported cleanup failures can accompany the original failure in AppRunError, rather than replacing it.

Ready/close hooks own per-window state; creation and browser event callbacks report observations. A renderer can navigate or crash while its native window remains alive. Page tasks and subscriptions must therefore also respect page lifetime, as described in commands and events.

Quit decisions and forced exit

OperationContract
context.quit(exit_code=0)Requests orderly shutdown; cancelable
context.exit(exit_code=0)Forces shutdown and process termination, including status zero
handle.close()Requests closing that window; subject to its close interceptor

The default LastWindowClosedPolicy::Quit requests shutdown after the last window closes; KeepRunning keeps the application running until an explicit quit or exit. Browser and child-view teardown must finish before normal runtime shutdown completes.

An orderly quit runs on_before_quit, closes windows through their close decisions, then runs on_will_quit after windows close. ApplicationQuitDecision::Prevent cancels the quit; a denied window close also cancels the request. Cancellation does not recreate windows that have already closed. on_quit observes the final exit code before runtime teardown and is not a veto point.

Forced exit bypasses cancelable decisions, including an already pending decision, but still performs the final quit notification and runtime cleanup. It is not equivalent to externally killing the process. Scheduled relaunches are started after cleanup and before process termination.

On successful ordinary zero-status quit, App.run() returns. A successful nonzero quit terminates with that status; forced exit terminates even for zero. Code following run() must not be the only place for required shutdown cleanup.

Commands

A command is a typed request/response operation across the renderer–host boundary. Payloads are serialized; a shared MoonBit type does not share memory or execute backend code in the renderer.

Contract and binding

APIContract
proton_contract.command[Request, Response](name)Declares a typed application route; does not register a handler
App.commands(register, targets?)Installs application command bindings for selected renderer targets
CommandRegistrar.bind(command, handler)Binds an async handler taking (CommandContext, Request) and returning Response
proton_client.invoke(command, request)Async frontend invocation; returns Response or raises ClientFailure
proton_rabbita.invoke(...)Adapts completion/failure to Rabbita commands

On the backend, Request must implement FromJson and Response must implement ToJson. The frontend requires the inverse conversions. Contract and registration errors are distinct from failures of a running request. Application route names must be unique and valid; descriptors are validated during binding and invocation.

The handler context identifies the caller and supports emit_to_caller. A handler may await backend work. Business outcomes such as validation rejection can be modeled in the response type rather than thrown as transport failures.

JavaScript interface

The injected bridge exposes application methods as window.__MoonBit__.app.<name>(request). Calls return promises; failures reject them. Application routes use app:; extension operations use ext:. An ordinary browser page has no injected native bridge.

Request scope and page invalidation

Each accepted request has its own command scope. Awaited work, child tasks, and deferred cleanup in that scope belong to the request. A handler or its scoped cleanup failing is a request failure (handler_failed), not an instruction to terminate the application. Runtime infrastructure failures remain application errors.

Navigation, renderer termination, or bridge failure invalidates the old page's pending requests. Once a bridge attempt has failed, that failed page cannot start new application commands until a new valid attempt is established. Initialization-time requests are permitted; waiting for every request until ready would prevent legitimate initialization work.

Caller cancellation and page invalidation stop waiting and request cancellation of outstanding work. Neither rolls back business side effects already performed. Use application-level operation identifiers or transactions when a retry must not duplicate an operation. Do not infer successful execution from successful message submission.

Cancellation

proton_client.invoke_with_callbacks returns a cancellation function. It cancels response observation and requests transport cancellation; late responses are ignored. Async invoke also cancels the pending request when its task is cancelled. The request scope described above governs the backend work; cancellation does not undo completed side effects.

Client failures

VariantMeaning
BridgeUnavailableNative bridge is absent
InvalidContractInvalid command/event descriptor
RemoteFailureBackend rejection with code, message and optional detail
TransportFailureCommunication failure
ResponseDecodeResponse does not decode as the declared type
RequestCancelledThe pending request was cancelled

Command error codes

RemoteFailure.code identifies the failure independently of its message:

CodeMeaning
invalid_payloadThe request does not match the command's input type
unknown_opThe requested operation is not registered
handler_failedThe command handler raised an error
host_closedThe command host has closed
permission_deniedThe page is not permitted to invoke the operation

Handle codes rather than parsing message text, and handle unrecognized codes as other remote failures. Backend diagnostics remain in application logs. Development mode also includes them in detail; message remains a caller-facing description. Expected business outcomes belong in the command's response type.

Resource limits

Proton does not impose a fixed payload-size limit on commands. Payloads are serialized as JSON and remain subject to memory and underlying transport constraints. Large messages increase serialization, copying and parsing costs.

Request concurrency has no global fixed admission limit in 0.4.0. Applications still need to bound expensive work according to their own resource and ordering requirements; the bridge does not serialize unrelated business operations into a transaction.

Events

Events are typed host-to-renderer notifications with no response value. proton_contract.event[Payload](name) declares a descriptor; it does not install a listener or retain previous notifications.

Destinations

APIDestination and lifetime
CommandContext.emit_to_caller(event, payload)The page that issued the current command
WindowContext.events()Produces a window event emitter
WindowEventEmitter.emit(event, payload)The associated window while it remains alive

Host payloads require ToJson. Window emitters belong to window lifetime; a saved emitter must be removed from application state when that window closes. Emission is not an implicit broadcast to all windows.

Subscriptions

proton_client.subscribe(event, listener, failure) decodes payloads through FromJson and returns a Subscription. Subscription.close() releases the listener. Subscription setup can raise ClientFailure; event decoding failures are reported as EventDecode through the failure callback.

proton_rabbita.subscribe integrates subscription ownership into Rabbita. Its options include a subscription key, retry count, ready command and client override; full signatures are in the Rabbita API.

The JavaScript interface is window.__MoonBit__.app.on(name, callback). The callback receives an event envelope containing payload; registration returns an unsubscribe function.

Delivery semantics

  • Notifications missed before subscription are not replayed.
  • Command responses and events are separate deliveries; their relative order is not an application synchronization contract.
  • Listener disposal ends observation; events are not a durable queue or acknowledgment protocol.
  • A notification that state changed can invalidate a frontend query. The authoritative snapshot is obtained through a command.

Subscription lifetime and state synchronization

A subscription belongs to the current renderer document or UI component. Close it when that owner is disposed. A reload creates a new document and requires a new subscription; keeping a native window alive does not preserve JavaScript listeners across reloads.

For state synchronization, subscribe before loading the initial snapshot, use events to invalidate that snapshot, and ignore responses from superseded queries. Subscribing first avoids a gap before the initial read, but does not turn two independent messages into an atomic transaction. Include a revision in application data when the consumer must detect stale snapshots or missed changes.

Application-level lifecycle notifications such as on_window_created and on_render_process_gone are host callbacks, not proton_contract frontend events. Register them on the App builder; use a command or explicit event if the frontend also needs that information.

The event tutorial demonstrates subscription and cleanup. The Todo tutorial demonstrates invalidation followed by a snapshot query.

Windows and browser views

Window declarations belong to App; operations on a running window belong to WindowHandle. Browser navigation and developer tools belong to WebContentsHandle, obtained through WindowHandle.web_contents().

Identity and declaration

The primary window ID is main. App.add_window(id, config, open_on_start?) declares secondary windows; their IDs must be nonempty, unique and different from main. Titles are display text and do not identify windows.

Secondary windows open at startup by default. open_on_start=false defers opening until WindowManager.open(id). ApplicationContext.windows() and WindowContext.windows() expose the manager. All windows belong to one application runtime.

WindowConfig(title, entry, ...) holds dimensions, size hint, theme, titlebar style, traffic-light position, and initial views. Use App(config) for the primary window, App.main_window(config) to replace its startup configuration, and App.add_window(id, config, ...) for secondary windows. Registrations snapshot the views array. The html, url, file, and asset shortcuts remain available for a simple primary window.

Opening and instance identity

WindowManager.open(id) is asynchronous and returns an activated WindowHandle. The id selects a declaration; it is not a permanent identity for every future native instance. After closing and reopening a declared window, obtain the new handle through open or find; an old handle does not become valid again.

Cancellation before activation commits discards the queued open or closes the instance created by that operation. After activation commits, the application owns the window: canceling the caller later does not close it. Unknown declarations and invalid window state produce window-session failures; task cancellation follows the async task's cancellation semantics.

hide() preserves the instance, browser, and associated work. close() initiates teardown and can be denied. Do not equate a close request with completed cleanup. Per-window state should be released by its lifecycle cleanup, not immediately after requesting close.

Lookup and browser readiness

WindowManager.find(id) returns the current active instance, including a window that is still starting. It does not wait for native browser initialization, page loading, or command bridge readiness. Obtaining a WebContentsHandle does not wait for these conditions either.

These are distinct conditions:

  • Native browser initialization makes browser queries such as WebContentsHandle.state() available. During startup, a query can raise WindowSessionError::OperationFailed because the browser is not initialized. Do not assume that all browser operations are queued until initialization completes.
  • Page loading concerns the current navigation. For work that needs a loaded page, observe on_web_contents_event for that window or view, then verify the expected URL and page state. DidFinishLoad reports a loading-to-idle transition; by itself it does not prove that the intended navigation succeeded. Handle LoadFailed separately.
  • Command bridge readiness concerns communication with the current page. A browser handle or a load-completion event alone does not establish that the bridge is ready.

Coordinate operations with the condition they actually require. A fixed delay after find() is not a readiness guarantee, and navigation or closure can invalidate earlier observations.

Runtime operations

AreaWindowHandle operations
Visibility and focusshow, hide, focus, is_visible, is_focused
Geometrybounds, set_bounds, position, set_position, content_size, set_content_size
Window stateminimize, maximize, restore, set_fullscreen
Appearanceset_theme, set_background_color, set_title, set_menu
Lifetimeclose

Native operations can raise WindowSessionError. A handle is not valid indefinitely: window-owned state and retained event destinations must be released when the window closes. A hidden window is still alive.

Close semantics

App.on_window_close_request registers an async callback that receives the window handle and returns WindowCloseDecision::Allow or Deny. window_lifecycle.on_close is cleanup after closure; it is not a veto hook. The default last-window policy is Quit. KeepRunning leaves the application running with no open windows and requires an explicit exit path. See application lifecycle.

Child browser views

WindowConfig.views declares initial views on either primary or secondary windows; WindowHandle.add_view creates one dynamically and returns ViewHandle. A view is a child browser hosted inside a window, not a second top-level window. Its bounds use a top-left origin; visibility and z-order are independent of the main page. remove_view removes a child. Closing the parent must also complete child-browser teardown.

Browser and view boundaries

Both WindowHandle.web_contents() and ViewHandle.web_contents() return WebContentsHandle. Navigation, script evaluation, zoom, audio, developer tools, printing, downloads and session access use that common handle. Child geometry, visibility, z-order and removal remain on ViewHandle; native window controls remain on WindowHandle. Child navigation does not navigate the main page.

Subscribe to App.on_web_contents_event for main-page and child-page events. Callbacks carry the common handle: window_id() identifies the owning window and view_id() is None for its main page. Handles remain bound to one native instance; reusing a declaration id does not revive an old handle. A child view does not receive the application command bridge.

on_render_process_gone covers both main pages and child views without requiring an extra on_web_contents_event subscription. Renderer termination invalidates page work; it is not a normal window close. Decide whether to reload or present recovery UI based on the reported details, and recreate page subscriptions after navigation.

A view declaration sets initial geometry; it does not provide automatic layout. Update its bounds when the parent content layout changes.

PDF output

WebContentsHandle.print_to_pdf(path, options?) is asynchronous and returns Unit after successful completion. Call it in an async context and handle failure at the call site; no completion-event subscription or request-id correlation is needed. Concurrent calls on main and child pages keep separate completion identities. Closing the page, losing its renderer, or ending the application wakes outstanding callers. Cancellation stops waiting, not the underlying native print job; the destination file may still be produced.

Platform behavior

WindowThemePreference controls a window's theme; system_appearance() reports system appearance. These are separate concerns. Titlebar styles and native controls differ by platform. Traffic-light positioning applies to macOS; the frontend layout must account for native controls when using an overlay titlebar.

Extensions and capabilities

Extensions register reusable host operations. A renderer capability installs an extension's backend and grants a permission scope to selected renderer targets. Adding proton_ext as a module dependency only makes code available; it does not install handlers or grant access.

Registration and targets

App.capability(capability, targets?) configures installation and access. With no explicit targets, the grant applies to the main entry. RendererTarget.entry(window=...) and RendererTarget.bundled(window=...) select entry or bundled-page targets for a named window. Permissions do not implicitly become global merely because windows belong to one application.

Application commands use app: routes; extension operations use ext:<extension>/<operation>. JavaScript can invoke an installed operation through window.__MoonBit__.core.invokeOp(route, request), which returns a promise.

Filesystem scope

proton_ext/fs.capability accepts PermissionRoot values pairing a host directory with permitted operations. The scope is configured by the backend; renderer requests cannot widen it.

PropertyBehavior
Relative root or request pathResolved against resource_dir()
Path outside allowed rootsRejected, including symlink escapes
Operation absent from root grantNot authorized by that grant
Text payloadsUTF-8

Supported filesystem operation names include read_file, write_file, mkdir, readdir, remove, rmdir, rename, realpath, exists, kind and size. Canonical path checks and operations are serialized within the extension.

Availability and errors

A missing capability leaves its route unavailable. An installed extension can still reject a request because of scope, invalid arguments, a platform limitation or an operating-system failure. These failures are reported through the command bridge; installation does not imply that every native operation will succeed.

Capability-specific scope types are documented in the extension API; platform coverage is summarized below.

Platform differences and selection

These are key boundaries of implemented 0.4.0 capabilities, not a promise of availability without desktop services:

CapabilitymacOSWindowsLinux
Files, paths, host HTTP, child processesSupportedSupportedSupported
Native dialogs, text clipboardSupportedSupportedDepends on desktop session / GTK and related backends
System notification extensionSupportedNot implementedNot implemented
TrayMenus and platform eventsClick, right-click, double-click and menusRequires AppIndicator / Ayatana and desktop support
Desktop sources and thumbnailsDisplays; thumbnails require screen-recording permissionVisible titled windows and GDI thumbnailsX11/RandR displays; thumbnails are null

Capability grants are separate from operating-system authorization. Declaring a screen or media capability does not grant OS privacy permission. Handle OS denial, unavailable backends and missing application grants separately. Check support where provided, such as the tray capability; menu events are more portable than platform mouse gestures.

Host networking and child processes

The net extension performs host HTTP requests and returns status, headers and UTF-8 text. It is not browser fetch, does not share the browser cookie jar and does not follow redirects automatically. Its text response is not a lossless binary transfer interface. Processes spawned by the process extension belong to its application lifetime: wait collects handles, kill still needs wait, and application shutdown cancels and reaps remaining children.

See the extension API for the complete inventory and scope types. Check target grants, OS permission and platform backend support independently.

Browser sessions

A browser session contains cookies, cache, authentication state and web storage. Obtain its handle from ApplicationContext.session() even before a window exists, or from any WebContentsHandle.session(). Both access the same application session. Closing a page does not invalidate the session, and clearing shared data affects the other pages using that profile.

Startup settings

App.session_partition(name) selects a persistent profile below the application's sessionData directory. It is application startup configuration, not a per-window incognito switch. Windows in the application runtime use that configured profile. Use stable application identifiers and partition names to preserve data across launches.

App.proxy(server, bypass?) configures Chromium's startup-wide proxy. It cannot be changed during the run. This controls browser traffic; it does not configure every MoonBit networking library in the host.

The on_session_created callback exposes the partition and resolved data path before application start hooks create their windows. Packaged resources and the browser profile are different paths; do not put writable profile data inside the application bundle.

Operations

APIEffect
get_cookiesAsynchronously reads matching cookies; optional URL and HttpOnly selection
set_cookieSets a cookie with URL, name, value and optional domain/path/security attributes
delete_cookies, flush_cookiesDeletes selected cookies or flushes persistent cookie storage
clear_cache, clear_storage_dataClears cache or selected web-storage kinds
clear_auth_cacheClears cached HTTP authentication
clear_certificate_exceptionsClears certificate exception state
close_all_connectionsCloses session connections

Operations raise SessionError and require a live application runtime, not a live page. Handles remain usable with no windows under KeepRunning; application teardown wakes pending cookie readers and invalidates retained session handles. Concurrent cookie reads are independent. A clearing operation does not erase application-owned files or implement an application logout protocol; invalidate backend credentials and UI state separately.

Full attributes and StorageDataKind variants are in the API reference.

Application updates

Application updates replace the distributed application, not the development CLI or Mooncakes dependencies. Configure App.update_channel(endpoint, public_keys, check_on_launch?, freshness_days?) before running the app.

Channel and checks

The endpoint must use HTTPS and there must be at least one trusted RSA public key. Automatic checking defaults to enabled; manifest freshness defaults to 30 days and must be positive. Keys use the format validated by updater public-key.

The automatic check runs after successful startup and reports failures through logging without making startup fail. For explicit checks, use ApplicationContext.check_for_update() and handle its result and errors. NotConfigured is distinct from UpToDate; a context used after its application ends no longer has an active channel.

An offered PendingUpdate has passed manifest trust, freshness and revision checks. It exposes version, revision, size and optional notes URL, but the artifact has not yet been downloaded.

Download and installation

PendingUpdate.download() writes the artifact into a private stage and verifies its size, digest and signature. It does not replace application files or exit on any platform. Repeated downloads of the same prepared revision are a no-op.

After downloading, call ApplicationContext.quit_and_install() when the user chooses to apply the update:

match context.check_for_update() {
  Available(update) => {
    update.download()
    context.quit_and_install()
  }
  UpToDate | NotConfigured => ()
}

The request follows the quit lifecycle. Preventing quit cancels installation and retains the download for another attempt. Once quit is accepted, Proton closes the runtime and completes cleanup before installation. If single-instance mode is enabled, its reservation stays held throughout replacement. Windows transfers the reservation to its NSIS installer before the old process exits; the installer releases it after installation. macOS and Linux retain the lock while replacing the application artifact, then release it before launching the replacement. Successful handoff terminates the old process, including when the exit code is zero.

The method returns when the request is accepted, not when the new version is ready. Missing downloads and conflicting exit or relaunch requests fail immediately; later installation or launch failures are reported through App::run(). A forced exit() supersedes a pending update request. A pending update exit refuses explicit release of a held single-instance lock; an application that already released its configured lock cannot request installation. Ordinary quit discards the download without installing. Cleanup failure prevents installation. Old managed update artifacts are removed after the replacement successfully starts.

Publisher responsibilities

Package a stable application identifier with a monotonically increasing revision. The updater metadata options in CLI package describe the artifact location, publication instant and revision. updater public-key only formats a public key; it does not generate keys, sign an update manifest or host an endpoint. OS signing/notarization and updater signature verification serve different checks; configuring one does not configure the other.

Process control and metrics

Application process control determines which instance owns the runtime and how later launches reach it. It is separate from window visibility and from the task metrics exposed by Chromium.

Single-instance ownership

App.single_instance() uses the application identity to select one owning process. Later instances forward URL, document or reopen activation and return after the primary loop accepts it. Acceptance does not wait for asynchronous launch handlers to complete. Forwarding has a five-second deadline; failure does not kill the primary or start another owner.

ApplicationContext.has_single_instance_lock() queries ownership and release_single_instance_lock() releases it during the run. Releasing does not terminate the process. Choose deliberately whether another process may now become the owner. Register launch-input handling for both initial and forwarded activations.

App state versus window state

Application focus, hide/show, active/hidden queries and readiness describe the application. A window handle controls one window. A hidden window remains alive, and an application with KeepRunning can remain alive with no windows. Always provide an explicit exit path for background applications.

Task metrics

ApplicationContext.task_metrics() returns AppTaskMetric rows for Chromium tasks. A renderer and multiple workers can share a process. Task ids identify tasks; the hosting process usage in multiple rows may be identical because it belongs to the shared process. Do not sum rows into a process or application total.

CPU is zero until a sampling interval has passed; 100% represents one fully used core. Memory is -1 before measurement, not zero bytes. The fields are process_cpu_percent and process_memory_bytes. These observations do not confer ownership of a process and should not be used to kill helpers independently of Proton's lifecycle.

API references

Create a project

This guide creates a desktop window with the minimal template. You will run it, change its contents, and build the executable. Finish prerequisites before starting.

1. Create the application

Run this command in the directory where you keep projects, not inside another MoonBit workspace:

proton_cli new hello-proton --template minimal --yes
cd hello-proton

The explicit --template minimal matters: the non-interactive default is isomorphic. --yes accepts generated defaults. For a distributable application, choose your identity at creation with --identifier com.example.hello-proton.

Template files

hello-proton/
  moon.mod
  proton.project.json
  app/
    moon.pkg
    main.mbt
  • moon.mod gives the module its name and declares versioned dependencies. Add a library here before importing its packages.
  • app/moon.pkg imports the packages used by the entry and marks it as a native executable.
  • app/main.mbt defines the async entry, HTML, and application builder.
  • proton.project.json tells the CLI which package to run and how to identify/package the app.

Module dependencies and package imports are separate. A dependency makes a module available; a package import selects the APIs and aliases used by that package.

2. Prepare and run

From hello-proton/:

moon update
proton_cli cef setup
proton_cli dev

A native window opens with the generated greeting. The terminal remains occupied while the app runs. Close the window before continuing.

The CLI builds the native executable and starts it with the configured project metadata. This template has inline HTML, so there is no frontend server to start.

3. Change the page

Replace the entire contents of app/main.mbt with:

///|
async fn main {
  let html =
    #|<!doctype html>
    #|<html lang="en">
    #|<meta charset="utf-8">
    #|<title>Hello Proton</title>
    #|<style>
    #|  body { font: 18px system-ui; padding: 32px; }
    #|</style>
    #|<h1>Hello from MoonBit</h1>
    #|<p>This page lives inside a desktop window.</p>
    #|</html>
  @proton.html("Hello Proton", html, width=900, height=700)
  ..config(@proton.load_config())
  .run_or_abort()
}

Run proton_cli dev again. You should see “Hello from MoonBit”, a short paragraph, and a 900 × 700 initial window.

@proton.html creates the app builder. Its first argument is the window title; the second is the HTML document. The #| lines form a MoonBit multiline string.

..config(@proton.load_config()) loads the complete application configuration, including its required identity, and installs it on the application. The display title alone is not an application identity.

.run_or_abort() runs until the app exits and reports startup/runtime failures. The entry is async, so retain the template's moonbitlang/async import.

4. Check and build

After closing the window:

moon check --target native
proton_cli build

A successful build creates the native executable; it does not yet assemble a distributable application. Build and distribute explains that next step.

If the app does not start, run proton_cli doctor and inspect the full terminal error. If it builds but shows the old page, stop the previous process and rerun dev; this template has no frontend hot-reload server.

Where to go next

To add interaction to this page, follow calling the backend. To write the UI in MoonBit too, use the complete isomorphic example.

Calling the backend

A command lets frontend code ask the native backend to do work. This guide adds a working greeting form to the minimal project. It uses plain JavaScript first so the native boundary is visible; the Todo tutorial uses shared MoonBit types and Rabbita.

Add the contract dependency

In moon.mod, add this entry inside the existing import { ... } block, preserving the existing dependencies:

"moonbit-community/proton_contract@0.4.0",

Replace app/moon.pkg with:

import {
  "moonbitlang/core/json",
  "moonbitlang/async",
  "moonbit-community/proton",
  "moonbit-community/proton_contract",
}

supported_targets = "native"

pkgtype(kind: "executable")

Run moon update from the project root. A module dependency makes the library available; the package import makes @proton_contract usable in the entry.

Define, register, and call a command

Replace app/main.mbt with this complete example:

///|
struct GreetRequest {
  name : String
} derive(FromJson, ToJson)

///|
let greet : @proton_contract.Command[GreetRequest, String] =
  @proton_contract.command("greet")

///|
async fn main {
  let html =
    #|<!doctype html>
    #|<html lang="en">
    #|<meta charset="utf-8">
    #|<title>Greeting</title>
    #|<style>body { font: 18px system-ui; padding: 32px; }</style>
    #|<label>Name <input id="name" value="MoonBit"></label>
    #|<button id="greet">Greet</button>
    #|<p id="result" role="status"></p>
    #|<script>
    #|  document.querySelector("#greet").onclick = async () => {
    #|    const result = document.querySelector("#result");
    #|    try {
    #|      result.textContent = await window.__MoonBit__.app.greet({
    #|        name: document.querySelector("#name").value
    #|      });
    #|    } catch (error) {
    #|      result.textContent = String(error);
    #|    }
    #|  };
    #|</script>
    #|</html>
  @proton.html("Greeting", html)
  ..config(@proton.load_config())
  ..commands(fn(registrar) raise {
    registrar.bind(greet, (_context, request) => {
      "Hello, " + request.name + "!"
    })
  })
  .run_or_abort()
}

Run proton_cli dev. Enter “Ada” and click Greet. The page should display “Hello, Ada!”.

There are three connected parts:

  1. GreetRequest describes the JSON request, and greet declares its route and response type.
  2. ..commands(...) registers a handler with registrar.bind. The handler receives the caller context and decoded request.
  3. window.__MoonBit__.app.greet(...) returns a JavaScript promise for the response. The page awaits it and handles rejection.

Declaring a descriptor alone does not register a handler. Keep command names unique within the application.

If the page opens in a normal browser, window.__MoonBit__ is absent. Start the desktop app with proton_cli dev, not just the frontend URL.

Arguments and return values

The object property name matches the request field. Add serializable fields to the request when an operation needs more input. Results can be strings, numbers, arrays, or structs with ToJson.

The native handler executes in the backend. It is the right place for business validation or native operations; the renderer does not directly call arbitrary MoonBit functions.

Errors

A malformed request, unknown command, or handler failure rejects the JavaScript promise. Keep the try/catch around the call and show a useful message.

Expected business outcomes should be response data. For example, the Todo template returns Changed, InvalidTitle, or MissingTodo. Its frontend handles these separately from ClientFailure, which describes bridge, transport, and decoding failures.

Do not assume a request succeeded just because the frontend button handler ran. Wait for its result before updating UI that depends on successful completion.

Async work and caller context

Command handlers can perform async work. The context identifies the caller and can send a typed event to it; sending events to the frontend extends this exact example.

Sending events to the frontend

Use events when the backend needs to notify a frontend about a change. This guide extends the greeting command from calling the backend: clicking the button still invokes a command, but the displayed message now comes from an event.

Declare the event

In app/main.mbt, add this declaration alongside the existing greet descriptor, outside main:

///|
let greeted : @proton_contract.Event[String] =
  @proton_contract.event("greeted")

The event name is greeted and its payload is a string. It has no response type: emitting an event does not ask the frontend to return a value.

Send to the caller

Inside ..commands(...), replace the existing registrar.bind(greet, ...) call with:

registrar.bind(greet, (context, request) => {
  let message = "Hello, " + request.name + "!"
  context.emit_to_caller(greeted, message)
  message
})

context.emit_to_caller sends to the page that invoked this command. The final message is still the command response. The response and event are separate deliveries; do not rely on a particular ordering between them.

Listen before triggering work

Replace the contents between <script> and </script> in the HTML string with the following JavaScript. In the MoonBit multiline string, prefix each line with #| as in the previous example:

let stopListening;
document.querySelector("#greet").onclick = async () => {
  const result = document.querySelector("#result");
  const app = window.__MoonBit__.app;
  try {
    if (!stopListening) {
      stopListening = app.on("greeted", ({ payload }) => {
        result.textContent = "Event: " + payload;
      });
    }
    await app.greet({ name: document.querySelector("#name").value });
  } catch (error) {
    result.textContent = String(error);
  }
};
window.addEventListener("pagehide", () => {
  stopListening?.();
});

Start the app, enter a name, and click Greet. The expected message is now “Event: Hello, Ada!”. Clicking again updates it without adding another listener.

The first click installs the listener before invoking the command, so this example does not emit its first notification before subscribing. app.on returns an unsubscribe function. Here it is kept until the page leaves; a component-based UI should release it when the owning component is disposed.

Notify a window outside a command

Background work may need to notify a window after the originating command has completed. The Todo template saves context.events() from window_lifecycle(on_ready=...) and removes that destination in on_close. The saved WindowEventEmitter can emit typed events while the window remains alive.

Choose destinations explicitly. Do not retain stale window emitters or treat every event as a global broadcast. See the complete Todo example for the attach/detach lifecycle.

Events and state

A listener can miss events while it is absent. For application state, use an event to invalidate data, then query a fresh snapshot. The Todo app subscribes before its initial query and refreshes on todos_changed.

Use command results for acknowledgement of requested work. Use events for notifications to observers. If a workflow requires durable delivery or replay, implement that in the application's state model rather than assuming the event bridge provides it.

Managing windows

This tutorial adds a secondary window to the minimal application, then controls its visibility and closing behavior. Start from that project with its existing package imports; replace its entry point with the code below.

Create a second window

Replace app/main.mbt with:

///|
async fn main {
  @proton.html("Main", "<h1>Main window</h1>", width=900, height=700)
  ..config(@proton.load_config())
  ..add_window(
    "help",
    @proton.WindowConfig(
      "Help",
      @proton.AppEntry::Html("<h1>Help</h1><p>Close this window to return.</p>"),
      width=480,
      height=320,
    ),
  )
  ..window_lifecycle(
    on_ready=context => {
      let id = context.handle().id()
      println("Ready: " + id)
      id
    },
    on_close=id => println("Closed: " + id),
  )
  .run_or_abort()
}

Run proton_cli dev. Both Main and Help should open. The terminal prints each window's ID when it is ready and when it closes. Closing Help leaves Main running; closing the last window exits the app.

The primary window's ID is main. Secondary IDs must be nonempty, unique, and different from main. The ID identifies a window in code; its title is display text.

Open a window on demand

add_window opens the declared window at startup by default. Set open_on_start=false to declare it without opening it. Later, use context.windows().open("help") from an application or window context.

For example, add open_on_start=false to the Help declaration above, and add this builder call before .run_or_abort() to open it from the application startup hook:

..app_lifecycle(
  on_start=context => { ignore(context.windows().open("help")) },
  on_shutdown=_ => (),
)

A later user action can call the same window manager operation from a retained application context. Do not create a second Proton runtime for a second window.

Use and release handles

context.handle() inside on_ready gives the current WindowHandle. It supports operations such as focus(), hide(), show(), and close(). Browser operations are available through handle.web_contents().

A lifecycle callback's return value becomes the corresponding on_close argument. The example returns an ID; the Todo template uses that ID to remove a saved event destination. Keep window-owned state in this lifecycle and stop using the handle after close.

Decide when the application exits

The default last-window policy quits the application. Tray and background applications can add:

..last_window_closed_policy(@proton.LastWindowClosedPolicy::KeepRunning)

With that policy, closing all windows does not stop the process. Provide an explicit quit action before using it in a real application.

Use on_window_close_request for an asynchronous close decision: return WindowCloseDecision::Allow to proceed or Deny to keep the window open. A confirmation or unsaved-document check belongs here; on_close is cleanup after closing.

Platform-specific window behavior

Native window decorations and available controls differ across platforms. Check the window API before relying on platform-specific methods. Keep frontend titlebar layout and native configuration together when using an overlay titlebar.

A secondary page is also a separate capability target. Grant only the host operations it needs; see native capabilities.

Using native capabilities

Extensions expose reusable host operations to the renderer. A capability installs an extension's backend handlers and grants access to a specific scope. This tutorial adds file reading to the minimal application: the backend grants one directory, and the page requests a text file inside it.

Add the filesystem extension

In the minimal project's moon.mod, add this dependency inside import { ... }:

"moonbit-community/proton_ext@0.4.0",

Use these imports in app/moon.pkg, then run moon update:

import {
  "moonbitlang/async",
  "moonbit-community/proton",
  "moonbit-community/proton_ext/fs",
}

supported_targets = "native"

pkgtype(kind: "executable")

Prepare a file

Create a directory named workspace at the project root. Inside it, create message.txt containing:

Hello from the filesystem.

Replace app/main.mbt with:

///|
async fn main {
  let html =
    #|<!doctype html>
    #|<html lang="en">
    #|<meta charset="utf-8">
    #|<title>Read a file</title>
    #|<button id="read">Read message.txt</button>
    #|<pre id="result" role="status"></pre>
    #|<script>
    #|  document.querySelector("#read").onclick = async () => {
    #|    const result = document.querySelector("#result");
    #|    try {
    #|      const reply = await window.__MoonBit__.core.invokeOp(
    #|        "ext:fs/read_file", { path: "./workspace/message.txt" }
    #|      );
    #|      result.textContent = reply.content;
    #|    } catch (error) {
    #|      result.textContent = String(error);
    #|    }
    #|  };
    #|</script>
    #|</html>
  @proton.html("Read a file", html)
  ..config(@proton.load_config())
  ..capability(
    @fs.capability([
      @fs.PermissionRoot("./workspace", ["read_file"]),
    ]),
  )
  .run_or_abort()
}

Run proton_cli dev and click Read message.txt. The page should display the file contents. This is an actual host filesystem read, not a browser file picker.

Understand the grant

The capability has three relevant choices:

  • Operation: only read_file is allowed; this page cannot use this grant to write or delete.
  • Root: only paths inside ./workspace are allowed.
  • Target: with no explicit targets, the grant applies to the main window entry.

The low-level route ext:fs/read_file belongs to the filesystem extension. Application commands use the separate app: route space. You do not register the filesystem handler yourself.

Relative roots and requests resolve against @proton.resource_dir(), which the CLI configures for the application. Do not assume they resolve against an arbitrary terminal working directory.

Check failures deliberately

Change the request path to a missing file within workspace: the call should fail and the catch block should show the error. Change it to a file outside the allowed root: the grant should reject access.

Removing ..capability(...) does not stop the app from starting, but the route becomes unavailable. Adding an extension dependency alone is not a permission grant.

Multiple windows and persistent files

For a second window, select explicit RendererTarget::entry(window="...") or RendererTarget::bundled(window="...") targets when adding the capability. Grant each page only what its feature requires.

The local workspace directory is useful for this development exercise. Installed resources may be read-only; use an appropriate writable application-data or user-selected directory for persistent files. Package files you intentionally ship via resources configuration.

Other capabilities

Dialogs, clipboard, shell, tray, and other extensions follow the same explicit installation/grant model, but each defines its own scope and platform support. The notification extension in this release targets macOS; do not infer platform support from the framework's overall platform list.

Consult the extension API for the capability builder and request/response types you need.

Build a complete Todo application

This tutorial uses the isomorphic template to connect a shared contract, native state, and a Rabbita UI. You will first run the generated Todo app, then add Complete all and Reopen all operations across all three modules.

Complete prerequisites, including Node.js for Warren's build tooling. You do not need to modify the earlier minimal app.

Create and explore the app

From outside another MoonBit workspace:

proton_cli new todo-app --template isomorphic --yes
cd todo-app
moon update
moon install moonbit-community/warren@0.4.5
proton_cli cef setup

The generated configuration already invokes the installed Warren executable. Start the application:

proton_cli dev

Add “Alpha” and “Beta”, mark one complete, and search for it. The generated app already supports create, complete, delete, and filtered queries. It keeps the list in memory, so restarting clears the data.

Close the application before editing. Keep the generated module names and package imports; all changes below extend the existing template.

Template organization

The isomorphic template has three modules connected by a workspace:

todo-app/
  moon.work
  proton.project.json
  shared/
    moon.mod
    moon.pkg
    todo_contract.mbt
  backend/
    moon.mod
    app/
      moon.pkg
      main.mbt
    todo/
      moon.pkg
      backend.mbt
      commands.mbt
  frontend/
    moon.mod
    main/
      moon.pkg
      main.mbt
    internal/query/
    public/

The root moon.work connects three modules:

  • shared defines serializable payloads and command/event descriptors. It is used by both build targets.
  • backend builds to native code. todo/backend.mbt holds Todo state and operations; todo/commands.mbt binds those operations; app/main.mbt starts the app.
  • frontend builds to JavaScript. main/main.mbt defines the Rabbita UI; public/ contains its HTML and stylesheet.

The template's frontend/internal/query manages query state and subscriptions. It belongs to the generated application and can be changed with it; it is not a public Proton API.

Application commands use ordinary bind calls; this template requires no command code-generation rule.

Understand the existing flow

For an existing create action, Create invokes create_todo; the backend validates and mutates; MutationReply reports success or a business rejection. todos_changed invalidates the frontend snapshot and triggers a query.

The frontend's draft is local UI state. The backend list is authoritative application state. The new action must mutate the full backend list, not just the rows currently visible under a search filter.

1. Extend the shared contract

Append to shared/todo_contract.mbt:

///|
pub(all) struct SetAllCompletedRequest {
  completed : Bool
} derive(ToJson, FromJson)

///|
pub extend SetAllCompletedRequest with ToJson::{to_json}

///|
pub extend SetAllCompletedRequest with FromJson::{from_json}

///|
pub let set_all_completed : @proton_contract.Command[
  SetAllCompletedRequest,
  MutationReply,
] = @proton_contract.command("set_all_completed")

The boolean chooses completion or reopening. We reuse the template's MutationReply, so the existing response handling remains useful. Both targets import this same descriptor.

2. Implement the state change

Append to backend/todo/backend.mbt:

///|
fn Backend::set_all_completed(
  self : Backend,
  completed : Bool,
) -> @shared.MutationReply {
  let mut changed = false
  for index = 0; index < self.todos.length(); index = index + 1 {
    let todo = self.todos[index]
    if todo.completed != completed {
      self.todos[index] = { ..todo, completed, }
      changed = true
    }
  }
  if changed {
    self.version += 1
  }
  @shared.Changed(version=self.version)
}

This operation traverses the full list and advances the revision once if anything changed. Repeating the same operation leaves the revision unchanged. It returns the current version even for an empty list.

3. Register the handler and send invalidation

Inside the existing Backend::register_commands method in backend/todo/commands.mbt, add another binding beside the existing bindings:

registrar.bind(@shared.set_all_completed, (_context, request) => {
  let reply = self.set_all_completed(request.completed)
  self.notify(reply)
  reply
})

Keep the old bindings. self.notify is the generated backend's existing helper: it sends todos_changed to the event destinations attached by the app's window lifecycle.

No new event type is needed. The state changed in the same way as another Todo mutation, so existing observers should refetch the same query.

4. Connect the frontend action

All changes in this step are in frontend/main/main.mbt.

Add one variant inside the existing enum Msg:

SetAllCompleted(Bool)

Add this arm inside update's existing match msg:

SetAllCompleted(completed) =>
  (
    { ..model, error: None, },
    @proton_rabbita.invoke(
      @shared.set_all_completed,
      { completed, },
      reply => emit(MutationReceived(reply)),
      error => emit(CommandFailed(error)),
    ),
  )

model, emit, MutationReceived, and CommandFailed already exist in this function and module. The success path reuses the template's mutation response handling; the error path keeps bridge failures visible.

Inside view, find div(class="todo-toolbar", [...]). Append these two buttons to its child array, after the existing Refresh button:

button(
  type_="button",
  on_click=emit(SetAllCompleted(true)),
  "Complete all",
),
button(
  type_="button",
  on_click=emit(SetAllCompleted(false)),
  "Reopen all",
),

The template already imports button, so no new import is needed. The boolean in the message travels through the request to the backend.

5. Check the behavior

Append this regression check to backend/todo/backend_wbtest.mbt:

///|
test "bulk completion changes the whole list once" {
  let backend = Backend()
  ignore(backend.create("Alpha"))
  ignore(backend.create("Beta"))
  assert_true(backend.set_all_completed(true) is @shared.Changed(version=3))
  assert_true(backend.snapshot("").todos.all(todo => todo.completed))
  assert_true(backend.set_all_completed(true) is @shared.Changed(version=3))
  assert_true(backend.set_all_completed(false) is @shared.Changed(version=4))
  assert_true(backend.snapshot("").todos.all(todo => !todo.completed))
}

From the project root:

moon check --target js,native
moon -C backend test todo --target native
moon -C frontend test --target js
proton_cli dev

Add two todos. Complete all should complete both, and Reopen all should reopen both. Search for only one todo and repeat the operation, then clear the filter: both items should have changed. Repeating Complete all should not advance the revision again.

If the backend changes but the display does not, check that you retained self.notify and the window attach/detach hooks. If the new command is unavailable, check its registration and restart the backend.

How the UI stays current

The template's todo_app creates a query for list_todos invalidated by todos_changed. Its application-local query helper subscribes before fetching and cancels superseded requests when input changes.

The mutation response refreshes the initiating UI; the event invalidates observers. These can overlap, so the helper tracks request generations and avoids letting an older result overwrite a newer search. This is application code in frontend/internal/query, not another server or a public Proton state-management framework.

Build the finished application

Stop development and run proton_cli build. Choose the following branch for your host OS. Run packaging commands from the todo-app project root.

macOS and Windows

For local acceptance, create an unsigned app artifact. On Windows, app means a directory containing the EXE and its dependencies, not a macOS bundle.

proton_cli package --release --format app --dry-run
proton_cli package --release --format app

Open the artifact path printed by the CLI under dist: the .app on macOS, or the application EXE inside the Windows output directory. Do not copy the EXE alone. This step needs no NSIS and does not perform distribution signing or notarization.

Linux

The default template supplies no icon. Save a valid PNG icon as icons/app.png inside the project and add this field to the existing package object in proton.project.json:

"icons": ["icons/app.png"]

Check that appimagetool --version runs. If necessary, install it in your user directory from an Ubuntu x64 shell. Extracting the packaging tool avoids requiring FUSE to run it:

mkdir -p "$HOME/.local/lib/proton-appimagetool"
cd "$HOME/.local/lib/proton-appimagetool"
curl -fL -o tool.AppImage https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
echo 'ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0  tool.AppImage' | sha256sum -c -
chmod +x tool.AppImage
./tool.AppImage --appimage-extract
ln -sf squashfs-root/AppRun appimagetool
export PATH="$HOME/.local/lib/proton-appimagetool:$PATH"
appimagetool --version

Return to the todo-app project root, then run:

proton_cli package --release --format appimage --dry-run
proton_cli package --release --format appimage

Launch the output AppImage in a graphical session. If the runtime environment cannot mount AppImages, run the artifact with --appimage-extract and launch AppRun inside the extracted directory.

Acceptance

With the development server stopped, launch the artifact, add two items, and verify Complete all, Reopen all, and search. Packaging failure, launch failure, and incorrect button behavior are separate stages; dry-run alone is not acceptance.

The list is intentionally in memory and resets on restart. Implement persistence in the backend. See packaging reference for icons and distribution requirements.

Command Line Interface

This page lists every proton_cli command and its options. Application IPC commands are documented separately in Commands. Configuration file fields are defined in project configuration.

Invocation and global options

proton_cli [--cwd <directory>] <command> [options]
OptionMeaning
-C, --cwd <directory>Working directory; default .; available to subcommands
-h, --helpHelp for the selected command
-V, --versionCLI version; equivalent in purpose to version

--config, --moon-target-dir and the doctor report path are resolved from the selected working directory. dev --frontend-path and package icon/output paths are resolved from the project configuration directory. Paths in a project configuration are resolved from that file; frontend.dist uses the frontend directory. Explicit command options override the corresponding configuration values.

Command index

CommandPurpose
helpShow command help
versionPrint CLI version
newCreate an application project
devBuild and run an application in development mode
buildBuild frontend assets and the native backend
packageBuild and assemble distribution artifacts
doctorReport environment and project diagnostics
cef setupInstall the release-matched runtime and helper
cef requirementsPrint the embedded CEF requirement
updater public-keyValidate and format a trusted updater public key

help

proton_cli help [command] [subcommand]

Displays help without executing the selected command. For example, proton_cli help cef setup describes runtime installation; it does not install anything. proton_cli <command> --help is the equivalent option form.

version

proton_cli version

Prints proton_cli <version>. It accepts no command-specific options and does not perform the update check.

new

proton_cli new [path] [options]

Creates project source files. Without --yes, interactive prompts are used outside CI. The defaults below apply when no prompt or explicit option overrides them.

Argument / optionDefault and meaning
[path]todo; destination directory
--template <name>isomorphic; accepts minimal or isomorphic
--title <text>Derived from the project directory name; application/window title
--author <name>username; MoonBit module author
--identifier <id>dev.proton.<normalized-project-name>; reverse-DNS application identifier
--width <pixels>960; positive integer
--height <pixels>640; positive integer
--check, --no-checkCheck enabled by default; runs moon check --target js,native after creation
--git, --no-gitExplicitly enable/disable Git initialization; non-interactive default does not initialize Git
-y, --yesSkip prompts and accept defaults
--dry-runPrint planned files without writing them

minimal creates a native module with inline HTML. isomorphic creates shared, frontend and backend modules. Invalid identifiers, nonpositive dimensions and unsupported template names are rejected.

dev

proton_cli dev [options] [-- <application-arguments>...]

Builds and runs the native backend. When a frontend command is configured, it starts that command, waits for readiness and supplies the development URL to the application. Backend changes require a rebuild/restart; frontend refresh is owned by the frontend tool.

OptionDefault and meaning
--config <path>Project configuration; defaults to proton.project.json in the working directory
--package <selector>Overrides the configured backend package; fallback app
--moon-target-dir <path>Override the internal Moon invocation's output directory
--url <url>Override frontend.dev_url
--command <command>Override frontend.before_dev
--frontend-path <path>Override the frontend command's working directory
--timeout-ms <milliseconds>30000; frontend readiness timeout
--ready-path <path>Use an HTTP readiness probe at this path; otherwise checks TCP connectivity
--no-frontendDo not start a frontend command; use an externally managed server
--setup, --no-setupAutomatic installation of missing runtime/helper; disabled by default
-- <arguments>...Arguments for the application, not for Moon

A CLI-managed frontend requires a command and rejects an already occupied development endpoint. HTTP probing requires curl. A readiness timeout prevents application launch. The CLI owns the frontend process it starts; an external server has a separate lifetime.

build

proton_cli build [options] [-- <moon-build-arguments>...]

Runs the configured frontend build, validates frontend output, then builds the native backend. A failed frontend command prevents the backend build. The result is build output, not a complete distribution.

OptionDefault and meaning
--config <path>Project configuration; defaults to proton.project.json in the working directory
--package <selector>Overrides the configured backend package; fallback app
--moon-target-dir <path>Override the internal Moon build's output directory
--no-frontendSkip the frontend build command; does not generate missing frontend assets
-- <arguments>...Forward to moon build, e.g. -- --release

The backend target is always native. A forwarded --target override is rejected.

package

proton_cli package [options]

Builds and assembles the application, frontend assets, CEF runtime, matching helper and declared resources for the host platform. A project configuration is required. This command does not accept trailing Moon arguments.

OptionDefault and meaning
--config <path>Project configuration; defaults to proton.project.json
--package <selector>Override backend package
--releaseUse release build output; otherwise debug
--dry-runResolve and print the plan without executing the full build/package flow
--product-name <name>Override package.product_name
--version <version>Override application version; does not select a Proton version
--format <format>Repeatable; app, zip, dmg, nsis, appimage, subject to host support
--output <directory>Override output directory
--icon <path>Repeatable; override configured icons
--url-scheme <scheme>Repeatable; override configured URL schemes
--nsis-install-mode <mode>currentUser, perMachine or both; overrides Windows configuration
--macos-minimum-system-version <version>Override package.platforms.macos.minimum_system_version
--signSign the application; disabled by default
--notarizemacOS signing, notarization and stapling; implies --sign
--updater-base-url <https-url>Emit update metadata using this artifact base URL; requires publication instant and revision
--updater-published-at <instant>Publication instant in YYYY-MM-DDTHH:MM:SSZ; requires base URL
--updater-revision <integer>Monotonic release sequence embedded in the app and update metadata

CLI lists such as --format override configured lists. Application identity comes from the top-level configuration identifier; package has no --identifier option. --release does not imply signing. A successful dry run does not verify artifact creation or runtime behavior. See packaging for platform formats, configuration defaults and resource rules.

Signing environment

Credentials are environment configuration, not command options.

VariableMeaning
PROTON_MACOS_SIGNING_IDENTITYRequired identity for macOS signing
PROTON_MACOS_ENTITLEMENTSOptional entitlements file
PROTON_MACOS_NOTARY_PROFILEmacOS notary credential profile; PROTON_NOTARY_PROFILE is the fallback
PROTON_MACOS_BUILD_NUMBEROptional bundle version override; one to three numeric components
PROTON_MACOS_ALLOW_ADHOC1 permits ad-hoc signing for local diagnostics
PROTON_WINDOWS_CERTIFICATERequired certificate path for Windows signing
PROTON_WINDOWS_CERTIFICATE_PASSWORDCertificate password; defaults to empty
PROTON_WINDOWS_TIMESTAMP_URLTimestamp service; defaults to http://timestamp.digicert.com; none disables it

doctor

proton_cli doctor [options]

Inspects the environment and current project without installing dependencies or repairing project files. It returns failure when diagnostics fail.

OptionMeaning
--verboseInclude detailed project diagnostics
--jsonRender one JSON document; conflicts with --quiet
--quietRender only failing diagnostics; conflicts with --json
--output <path>Write the report to a file

cef setup

proton_cli cef setup

Installs the CEF runtime and helper selected by this Proton release in the shared user-level stores and prints the runtime root. It accepts no command-specific options, writes no project runtime-selection file and does not select an arbitrary CEF version. Installation requires network access when artifacts are missing.

cef requirements

proton_cli cef requirements

Prints the release's embedded CEF requirement as JSON. It does not install the runtime or inspect an application's running browser. No command-specific options.

updater public-key

proton_cli updater public-key --modulus <hex> [--exponent <hex>]

Validates a trusted RSA public key and prints a comment with its bit length followed by rsa-sha256:<modulus>:<exponent>, suitable for App::update_channel's public_keys.

OptionMeaning
--modulus <hex>Required RSA modulus; accepts the Modulus= prefix produced by OpenSSL
--exponent <hex>Public exponent; default 010001 (65537)

Surrounding whitespace is trimmed and hexadecimal is normalized to lowercase. The runtime's key parser validates the key, including modulus size and exponent. This command does not generate a key pair, read PEM files, sign artifacts or publish updates.

Exit status and update checks

StatusMeaning
0Command succeeded
1Execution failed, or diagnostics reported failure
2Argument parsing failed

Commands other than version perform a best-effort CLI update check. PROTON_NO_UPDATE_CHECK=1 disables it. An unavailable update service does not prevent the command from running.

Packaging reference

The Proton CLI packages a project into a distributable containing its executable, frontend assets, CEF runtime, matching helper and declared resources. A backend executable alone is not a complete distribution. Packaging targets the host platform.

Field types and defaults are defined in Configuration.

Platform overrides

Each platform object accepts formats, resources and sign. Windows additionally accepts nsis_install_mode; macOS accepts minimum_system_version. A platform format list replaces the shared list. Platform resources are combined with shared resources. Path values use the project configuration directory as their base.

PlatformFormatsDefault
macOS Apple Siliconapp, zip, dmgapp, zip
Windows x64app, zip, nsisapp, zip
Linux x64appimageappimage

Icon and tool prerequisites

ArtifactIcon and tooling
macOS app / zipICNS for the application icon; native Apple tooling for builds and signing
macOS dmgUses system hdiutil to create an image from the app
Windows app / zipWindows SDK rc.exe embeds ICO into the EXE; PNG is not automatically converted
Windows nsisAlso requires NSIS makensis
Linux appimageRequires a PNG icon and executable appimagetool on PATH

The default scaffold does not provide icons. On Linux, missing PNG validation fails before invoking AppImage tooling. See the Todo packaging steps for preparation. Supply the platform's icon format rather than expecting Windows to convert icons intended for other platforms.

NSIS installation mode

ValueBehavior
currentUserDefault; current user, no administrator installation required
perMachineAll users, elevation required
bothInstaller offers a scope choice; may prompt for elevation even for current-user installation

Changing the mode is not an installation-scope migration. Updates to an existing machine-wide installation should retain its intended scope.

Minimum macOS version

minimum_system_version only writes macOS LSMinimumSystemVersion metadata. It does not make a binary built against a newer SDK/CEF compatible with older systems. Validate the complete artifact on the minimum supported OS; compilation alone is insufficient.

Signing and notarization

--release controls build mode and does not imply signing. On macOS, --sign requests signing; --notarize requests notarization, stapling and validation using configured credentials. Identity and credential environment variables are listed in the CLI reference.

An unsigned or locally signed artifact does not establish a trusted publisher identity. Credentials are deployment configuration, not application source.

Runtime resource behavior

Packaged frontend files load without the development server. Backend resource paths resolve through resource_dir(). Installed resources may be read-only; persistent application data belongs outside them.

--dry-run validates and displays the packaging plan. It does not prove that packaging, signing or the packaged runtime works. The Todo tutorial includes the application build and package exercise.

Diagnostics reference

Diagnostics come from distinct layers: project/toolchain validation, native startup and lifecycle, bridge requests, and frontend rendering. A successful frontend preview does not verify native operations; a successful build does not verify packaged execution.

Diagnostic interfaces

InterfaceScope
proton_cli doctorRead-only project, toolchain, runtime and helper checks
proton_cli --version / moon versionTool versions
WebContentsHandle.open_devtools()Chromium inspector for a browser
App.run()Typed application execution errors
App.run_or_abort()Error reporting followed by abort on failure
ClientFailureFrontend contract, bridge, transport and decoding failures

WindowHandle.web_contents() provides the browser handle. DevTools belongs to that browser's lifetime. Opening it from a window-ready hook must preserve any existing hook state and cleanup behavior.

CDP endpoint discovery

Set PROTON_REMOTE_DEBUGGING_PORT=0 when launching the native application to let Chromium select an available port. Read the actual browser WebSocket endpoint from its process output:

DevTools listening on ws://127.0.0.1:<port>/devtools/browser/<id>

Use the announced endpoint for that process lifetime; do not reserve and release a port before launch or assume a fixed port. An explicit port remains supported. This is Chromium's diagnostic endpoint, not Proton's application command transport.

The application must have debugging enabled (App.debug()); the port variable does not enable it. For native MoonBit automation, see the optional proton_cdp library.

Logs

Proton uses tonyfettes/xlog. Development output uses stderr; packaged applications use the platform log directory. Application categories use app.*; framework categories use proton.*. Application level and category settings are preserved.

Environment variableEffect
MOON_XLOGxlog filtering
PROTON_LOG_OUTPUT=stderrSelect stderr output, including for terminal-launched packaged apps
PROTON_CEF_LOGTemporary switch for separate CEF internal diagnostics; disabled by default

File output depends on packaged metadata. CEF diagnostics are not the application logging interface.

Locate the actual log

Packaged applications write proton-<pid>.log by default. Directories use the application identifier, not its display name:

PlatformDefault directory
macOS~/Library/Logs/<identifier>/
Windows%LOCALAPPDATA%&lt;identifier>\Logs\
Linux$XDG_STATE_HOME/<identifier>/logs/, or ~/.local/state/<identifier>/logs/ when unset

App.path(AppPathKind::Logs) returns the resolved path. Use that result when set_path or set_app_logs_path overrides the default. PROTON_LOG_OUTPUT=stderr redirects a terminal-launched packaged app to the terminal; unpackaged apps cannot use file mode.

Diagnose by stage

SymptomCheck and next action
CLI cannot find runtimeCompare proton_cli --version with the application's proton dependency; inspect cef requirements and run cef setup; retain the complete setup error
dev never reaches frontend readinessRun before_dev inside frontend.path, check dev_url host/port; use --no-frontend for an existing service instead of starting a second one
No bridge in a normal browserLaunch the native application through proton_cli dev; browser previews have no host bridge
permission_denied / unknown_opCheck bound descriptors, capability targets and current window/page; adding a module dependency is insufficient
Frontend command not foundCheck that the configured tool is installed and on PATH; Warren is installed separately from Proton CLI
Request or response decode failureCompare the command descriptor types with the actual JSON payload; inspect ClientFailure and backend logs
Packaged resource 404Inspect the artifact resource tree and relative HTML URLs, frontend.dist and package.resources; retry without a development server
Process remains after window closeCheck KeepRunning and denied quit decisions first, then final exit/cleanup errors; record process state and do not count manual killing as successful shutdown
appimage packaging failsCheck the PNG and appimagetool --version; distinguish icon validation from create AppImage tool failures

Reporting a problem

Include the command and full error, Proton and MoonBit versions, OS and architecture, and whether the failure occurs in development or in a packaged application. Attach proton_cli doctor --json output, the relevant application log, and a minimal reproduction. For shutdown failures, record whether the process exited on its own or was terminated manually. Review logs for application data or credentials before sharing them.

Project configuration

proton.project.json contains application identity and CLI build/package metadata. It is a JSON object with four recognized top-level fields. Unknown fields are rejected. Window state, command registration and capabilities belong to the MoonBit application builder, not this file.

The default filename is proton.project.json. To use another filename such as moon.proton.json, pass --config moon.proton.json to dev, build or package; it is not an automatically discovered alias.

Top-level fields

FieldTypeRequirement / meaning
identifierstringRequired, validated application identifier
backendobjectOptional at decoding; provides the CLI backend location and entry
frontendobjectOptional frontend command and asset configuration
packageobjectOptional distribution metadata; required for metadata-driven packaging

identifier is trimmed and must have at least two nonempty dot-separated components. Each component starts with an ASCII letter or digit and contains only ASCII letters, digits or hyphens.

A minimal configuration is:

{
  "identifier": "com.example.app",
  "backend": { "path": ".", "package": "app" }
}

Backend

When backend is present, both fields are required.

FieldTypeInterpretation
pathstringDirectory, resolved relative to the configuration directory
packagestringMoon package selector used from that directory

Frontend

All frontend fields are optional at decoding. Individual CLI commands impose additional requirements.

FieldTypeDefault / interpretation
pathstringConfiguration directory if omitted; command working directory
dev_urlstringNo default; development page URL
before_devstringNo default; frontend server command
before_buildstringNo default; frontend build command
diststringNo default; output directory, relative to frontend.path when specified

dev requires a frontend command when it manages a configured development URL. --no-frontend connects to an externally managed server. build runs the configured build command before validating the frontend output and building the backend. An existing occupied development endpoint is rejected when the CLI is responsible for starting its server.

Paths and resources

Backend/frontend paths, package icons, package resources and package output are resolved from the configuration directory. frontend.dist is resolved from the frontend directory. Absolute paths remain absolute. Path separators follow the host operating system. Loading configuration preserves path strings and does not require referenced files or directories to exist; build and packaging operations check the inputs they use.

@proton.resource_dir() is the application resource base: the CLI supplies the project root in development; packaged applications use their resources directory; direct runs without managed metadata use the startup working directory. Installed resources are not a persistent writable data store.

package

These fields belong to the package object in proton.project.json.

FieldTypeDefault / meaning
product_namestringRequired display name
versionstringRequired application version; independent of Proton's version
formatsstring arrayHost defaults when omitted
iconsstring arrayEmpty; icon paths relative to configuration directory
preparestringAbsent; preparation command
resourcesstring arrayEmpty; additional packaged resources
sign.binariesstring arrayEmpty; additional binaries selected for signing
url_schemesstring arrayEmpty; registered application URL schemes
document_typesobject arrayEmpty; document associations
outputstringdist, relative to configuration directory
platformsobjectOptional macos, windows, linux overrides

A document type contains required name and extensions; role defaults to Viewer. The canonical identifier is the top-level identifier, not a package field. Changing the product name does not change application identity.

package.sign

FieldTypeDefault / meaning
binariesstring array[]; additional binaries to sign, relative to the configuration directory

Omitting sign adds no extra binaries. This object does not enable signing: use package --sign or --notarize.

package.document_types[]

FieldTypeDefault / meaning
namestringRequired document type display name
extensionsstring arrayRequired nonempty list of file extensions
rolestringViewer; document role passed to packaging

package.platforms

FieldTypeDefault / meaning
macosobjectAbsent; macOS overrides
windowsobjectAbsent; Windows overrides
linuxobjectAbsent; Linux overrides

Each platform object accepts the following fields. Unknown fields are rejected, including nsis_install_mode outside Windows.

FieldTypeDefault / meaning
formatsstring arrayShared package.formats; an explicit list replaces it
resourcesstring array[]; appended to shared resources, duplicates removed
signobjectAbsent; accepts only binaries (string array, default []), appended to shared signing inputs with duplicates removed
nsis_install_modestringWindows only; currentUser (default), perMachine, or both
minimum_system_versionstringmacOS only; major.minor[.patch]. Sets LSMinimumSystemVersion; omitted when unset. Does not change compilation targets or validate binary compatibility.

If the resolved format list is empty, the host default formats apply. CLI options override the resolved configuration. See packaging behavior for supported formats, signing and installation modes.

Configuration ownership and directory layout

Proton does not require the directory layout of either scaffold template. The CLI locates the backend, frontend and resources through this file; see the path resolution rules under Paths and resources below.

File or entryResponsibility
moon.modModule name and versioned dependencies
moon.pkgPackage imports, targets and executable entry declarations
moon.work (when using multiple modules)Groups modules into a workspace
proton.project.jsonApplication identity, CLI build paths and packaging metadata
MoonBit App builderWindows, commands, capabilities and lifecycle behavior

A module dependency makes a module available; a package import selects the APIs used by that package. backend.package selects the build entry and does not replace Moon's package declarations.

Build output

Moon writes build artifacts under _build/ by default. Frontend output depends on the frontend build tool and should correspond to frontend.dist. frontend/dist is an isomorphic template choice, not a fixed framework path. The packager writes distributables to package.output, which defaults to dist.

These directories hold generated artifacts determined by source, build commands and project configuration. The runtime and helper installed by setup live in shared user-level storage, outside the application source tree.

Complete configuration example

The paths below are illustrative: referenced icons, resources and commands must exist in the application project.

{
  "identifier": "com.example.notes",
  "backend": { "path": "backend", "package": "app" },
  "frontend": {
    "path": "frontend",
    "dev_url": "http://127.0.0.1:4300",
    "before_dev": "warren dev --browser-entry main --direct --port 4300",
    "before_build": "warren build --browser-entry main",
    "dist": "dist"
  },
  "package": {
    "product_name": "Notes",
    "version": "1.0.0",
    "formats": ["app", "zip"],
    "icons": ["icons/app.icns", "icons/app.ico", "icons/app.png"],
    "prepare": "node scripts/prepare.mjs",
    "resources": ["resources"],
    "sign": { "binaries": [] },
    "url_schemes": ["notes"],
    "document_types": [
      { "name": "Note", "extensions": ["note"], "role": "Editor" }
    ],
    "output": "dist",
    "platforms": {
      "macos": { "formats": ["app", "dmg"] },
      "windows": { "formats": ["nsis"], "nsis_install_mode": "currentUser" },
      "linux": { "formats": ["appimage"] }
    }
  }
}

Runtime configuration

Load configuration independently, then install it on the application:

let config = @proton.load_config()
let app = @proton.html("Hello", "<h1>Hello</h1>")..config(config)

if config.frontend is Some(frontend) {
  if frontend.dev_url is Some(url) {
    println(url)
  }
}

load_config() reads and validates the complete typed AppConfig, raising AppConfigError immediately on failure. Development uses proton.project.json; packaged applications use proton-package.json. For an explicit file path, use @proton.AppConfig::load(path). Applications can also construct AppConfig directly.

Read fields from the configuration value. Nested sections are optional structs: config.frontend contains dev_url, path, dist, before_dev, and before_build. JSON package maps to config.package_config; JSON backend.package maps to the backend's app_package field.

proton_cli dev supplies the effective configuration after --url, --command, --frontend-path, and --package overrides. Packaging likewise embeds the effective configuration, including CLI overrides for formats and platform settings. Relative paths retain their spelling; reading a field does not resolve paths or execute commands.

app..config(config) installs an independent copy and is mutually exclusive with app..identifier(...). Invalid configuration or a mismatch with the packaged identity prevents startup. Retain config to inspect it; there is no application configuration getter. app.name() and app.version() read the installed snapshot without further file I/O.

Minimal application

This example starts a native window from an inline HTML string. It shows the smallest application entry point: select page content, assign an application identifier, and run the application.

01_run · Source files: main.mbt

Behavior

A single 800 × 600 window displays “01 run”. The builder declares identity and starts the application.

async fn main {
  let html =
    #|<!doctype html>
    #|<html>
    #|  <head><meta charset="utf-8"><title>01 run</title></head>
    #|  <body><h1>01 run</h1></body>
    #|</html>
  @proton.html("01 run", html, width=800, height=600, debug=true)
  ..identifier("dev.proton.01-run")
  .run_or_abort()
}

Implementation

The HTML is a MoonBit value passed directly to the application builder. No frontend server, asset lookup or command bridge is needed for this page. The explicit identifier supplies application identity for direct repository execution; a scaffold instead obtains it with load_config().

Reuse this form for a small self-contained page. Move to an asset entry when the page needs independently built CSS/JavaScript files. debug=true is an example choice, not a distribution requirement.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 01_run --target native

Limits and interpretation

Use this to isolate runtime setup from frontend tooling. There is no command bridge or external resource loading in the example.

Embedded HTML

This example keeps HTML in a separate source file and embeds it in the executable as a MoonBit string. It demonstrates how to maintain page content separately without requiring a runtime HTML file.

12_embed · Source files: main.mbt, hello.html, hello.mbt, moon.pkg

Behavior

The window loads the embedded resource through html(). The package prebuild rule produces hello.mbt from hello.html.

async fn main {
  @proton.html("12 embed", resource, width=800, height=600, debug=true)
  ..identifier("dev.proton.12-embed")
  .run_or_abort()
}

Implementation

resource is generated from hello.html by the package's embed prebuild rule. The runtime receives the same kind of string as the minimal example; it does not read hello.html from disk at launch.

Edit hello.html and rebuild. Do not edit generated hello.mbt. Embedding a document does not automatically embed every relative image or script it references; use an asset entry for a directory of resources.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 12_embed --target native

Limits and interpretation

Edit hello.html, not generated hello.mbt. Embedded HTML does not automatically include sibling files; use the sidecar example for separate JS/CSS.

Filesystem capability

This example lets a renderer request host file operations through the filesystem extension. The backend grants a specific directory and operation scope; the page exercises that grant through the command bridge.

18_extension_fs · Source files: main.mbt, fs.html

Behavior

The page exposes read, write and directory operations. The native entry registers the fs capability with a PermissionRoot and an operation allowlist.

async fn main {
  @proton.html("18 extension fs", resource, width=960, height=720, debug=true)
  ..capability(
    @fs.capability([
      @fs.PermissionRoot(".", [
        "read_file", "write_file", "exists", "kind", "size", "readdir", "mkdir",
        "remove", "rmdir", "rename", "realpath",
      ]),
    ]),
  )
  ..identifier("dev.proton.18-extension-fs")
  .run_or_abort()
}

Implementation

The backend installs the filesystem extension and chooses both its root and allowed operations. The renderer supplies a path and operation, but cannot grant itself a wider root. A missing file and an unauthorized operation are separate failure cases.

This example grants write/delete access for demonstration. In an application, narrow the list to the required operations and use a deliberate data directory. The page's HTML and JavaScript demonstrate the raw extension bridge; MoonBit frontends can use typed descriptors.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 18_extension_fs --target native

Limits and interpretation

The example grants write and deletion operations under its working directory. Use a disposable directory for experiments; production apps should grant only their required paths and operations.

Typed events

This example sends progress notifications while an asynchronous command runs. It demonstrates the separate roles of a command response and an event subscription when displaying ongoing backend work.

40_event_broadcast · Source files: main.mbt, app.html

Behavior

Starting the ticker produces tick events and a done event. The command also returns its collected result. The run_id distinguishes runs.

Implementation

Excerpt from the entry point; use the source link above for the complete context.

async fn run_ticker(
  context : @proton.CommandContext,
  payload : StartPayload,
) -> TickerResult {
  let count = clamp(payload.count, 1, 20)
  let interval_ms = clamp(payload.interval_ms, 100, 2000)
  let ticks : Array[TickEvent] = []
  for index in 1..<=count {
    @async.sleep(interval_ms)
    let tick = TickEvent::{
      run_id: payload.run_id,
      index,
      total: count,
      remaining: count - index,
    }
    ticks.push(tick)
    context.emit(tick_event, tick)
  }
  let done = DoneEvent::{ run_id: payload.run_id, total: count, }
  context.emit(done_event, done)
  TickerResult::{ ticks, done, }
}

A typed extension binds start to run_ticker. The handler awaits between ticks and emits tick/done notifications before returning a final TickerResult. Progress events and the command result have separate roles: a displayed tick is not completion of the command.

The count and interval are clamped by the backend. Reuse the request id in your UI to distinguish overlapping runs, close subscriptions with their owner, and handle request cancellation. Despite the historical directory name, this is not a durable broadcast log.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 40_event_broadcast --target native

Limits and interpretation

Events and command responses serve different purposes. Register listeners before starting a run; an event is not a durable history or an acknowledgement.

Multi-window commands

This example exposes command operations to two windows in one application. It shows how window declarations, renderer grants and caller identity work together without giving every page implicit access.

45_bridge_multi_window · Source files: main.mbt

Behavior

The identify command returns both the supplied label and CommandContext.window_id(). Calls from different windows retain their originating window identity.

Implementation

Excerpt from the entry point; use the source link above for the complete context.

async fn main {
  @proton.html(
    "Bridge Multi A",
    page_html("A"),
    width=520,
    height=420,
    debug=true,
  )
  ..add_window(
    "secondary",
    @proton.WindowConfig(
      "Bridge Multi B",
      @proton.AppEntry::Html(page_html("B")),
      width=520,
      height=420,
    ),
    open_on_start=false,
  )
  ..capability(@proton_extension.capability(multi_window_extension()), targets=[
    @proton.RendererTarget::entry(),
    @proton.RendererTarget::entry(window="secondary"),
  ])
  ..app_lifecycle(
    on_start=async fn(context) { ignore(context.windows().open("secondary")) },
    on_shutdown=fn(_state) {  },
  )
  ..identifier("dev.proton.45-bridge-multi-window")
  .run_or_abort()
}

One extension implementation serves two independently authorized renderer targets. The secondary declaration has open_on_start=false and is opened explicitly in the application start hook. The handler returns context.window_id(), proving that caller identity comes from the host context rather than the page's label.

Reuse shared business handlers, but list each intended target explicitly. Keep per-window UI state on the frontend and shared state in the host. Delayed calls can overlap; do not infer response ownership from whichever window currently has focus.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 45_bridge_multi_window --target native

Limits and interpretation

The optional delay_ms makes overlapping requests visible. A logical window name identifies an application window, not a globally reusable native handle.

HTML with sidecar assets

This example loads an HTML entry together with separate JavaScript, CSS and worker files. It demonstrates the resource layout needed when a page is served from application assets rather than a development server.

46_asset_sidecar_resources · Source files: main.mbt, app/app.html, app/app.js, app/app.css, app/worker.js

Behavior

asset() loads app.html with its sibling resources. The example also contains a second page for navigation.

async fn main {
  let app = @proton.asset(
    "Asset Sidecar Resources",
    "46_asset_sidecar_resources/app/app.html",
    width=760,
    height=500,
    debug=true,
  )..capability(@proton_extension.capability(extension()))
  app..identifier("dev.proton.46-asset-sidecar-resources").run_or_abort()
}

Implementation

The asset entry names an HTML document within the resource tree. Its neighboring scripts, styles and worker source remain separate files, so their relative URLs must remain valid after packaging. The extension registration is independent of asset selection.

When adapting this example, keep the resource tree layout and configure packaging to include it. Verify a packaged application with the development server stopped. A working development URL does not demonstrate that sidecar files were packaged.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 46_asset_sidecar_resources --target native

Limits and interpretation

Run from the examples directory so the relative asset path resolves correctly. Deployments must package the complete resource tree, not only the HTML file.

Native application menu

This example builds a native application menu and updates its command state while the window is running. It connects menu actions to a live window handle and releases that reference when the window closes.

49_app_menu · Source files: main.mbt

Behavior

Menu actions update enabled, visible and checked state. Native menu roles and application commands are declared in MoonBit.

Implementation

Excerpt from the entry point; use the source link above for the complete context.

async fn main {
  @proton.html(
    "App Menu State Review",
    html(),
    width=900,
    height=640,
    debug=true,
  )
  ..menu(menu_bar())
  ..identifier("dev.proton.49-app-menu")
  ..commands(register_commands)
  ..window_lifecycle(
    on_ready=context => {
      let window = context.handle()
      window_slot.val = Some(window)
      window
    },
    on_close=fn(_window) { window_slot.val = None },
  )
  .run_or_abort()
}

The menu is native UI configured on the builder. Command handlers change or inspect its state through a live window handle. The ready hook stores that handle; the close hook clears it so later commands cannot deliberately reuse a closed instance.

Keep stable menu command ids separate from translated labels. Native role items delegate standard platform behavior, while application command items need application handling. For multiple windows, replace the example's single window slot with explicit ownership per window.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 49_app_menu --target native

Limits and interpretation

Menu placement and supported roles depend on the desktop platform. A menu command may have no focused window; handlers must account for that.

Embedded browser view

This example places a child browser beside a sidebar in the main window. It demonstrates the distinction between the main page and a separately navigable browser view, including explicit bounds for the child.

53_view_minimal · Source files: main.mbt

Behavior

WindowConfig.views adds a browser at x=288 with an 832 × 720 viewport. It loads example.com separately from the host HTML.

Implementation

Excerpt from the entry point; use the source link above for the complete context.

async fn main {
  @proton.App(
    @proton.WindowConfig(
      "Minimal View",
      @proton.AppEntry::Html(sidebar),
      width=1120,
      height=720,
      views=[
        (
          "browser",
          @proton.view("https://example.com/", width=832, height=720, x=288),
        ),
      ],
    ),
  )
  ..debug(enabled=true)
  ..identifier("dev.proton.53-view-minimal")
  .run_or_abort()
}

The main browser renders the sidebar; WindowConfig.views creates a second browser inside the same native window. x=288 reserves the sidebar width, and the child loads example.com independently. This is useful for a host shell with an embedded page, rather than a separate top-level window.

The example intentionally has fixed geometry. A production layout must recompute child bounds after resizing and decide how remote navigation is handled. Granting commands to the local host page does not mean remote child content should receive the same capabilities.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 53_view_minimal --target native

Limits and interpretation

The remote page requires network access. Bounds are fixed in this minimal example; responsive view layout belongs to the application. Parent shutdown ends the child lifetime.

Application locale

This example sets the application locale at startup and observes it through commands, Chromium and native menus. It illustrates the framework locale setting; translating application-owned text remains the responsibility of the application.

56_i18n · Source files: main.mbt, app.html

Behavior

The app selects zh-CN and displays backend preferences alongside navigator.language and navigator.languages.

Implementation

Excerpt from the entry point; use the source link above for the complete context.

async fn main {
  let locale = @proton.Locale::parse("zh-CN") catch {
    error => abort(error.message())
  }
  @proton.html("Proton 国际化", resource, width=860, height=600, debug=true)
  ..locale(locale)
  ..capability(@proton_extension.capability(i18n_extension()))
  ..identifier("dev.proton.56-i18n")
  .run_or_abort()
}

The application explicitly parses zh-CN and passes it to locale(). The extension reads the resulting locale preferences from its command context; Chromium and native framework labels use the same startup choice. Windows uses a separate entry variant without the menu declared for other hosts.

This selects framework/browser locale; it does not translate arbitrary application strings. Keep application text in your own catalog. Locale is a startup choice in this example, not a live language-switch implementation.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 56_i18n --target native

Limits and interpretation

Proton propagates locale; it does not translate application text. This example omits native menus on Windows; do not infer menu parity from its other platforms.

Background residency

This example combines background residency with single-instance activation. Closing the last window keeps the application alive, so a later launch can reach the existing instance instead of starting another runtime.

57_background_residency · Source files: main.mbt

Behavior

KeepRunning retains the process. A second launch delivers Reopen; the handler recreates main only when it is absent.

Implementation

Excerpt from the entry point; use the source link above for the complete context.

async fn main {
  @proton.html("Background Residency", page, width=760, height=480)
  ..identifier("com.example.proton.background-residency")
  ..single_instance()
  ..last_window_closed_policy(@proton.LastWindowClosedPolicy::KeepRunning)
  ..on_launch_input(async fn(context, input) noraise {
    if input is Reopen && context.windows().find("main") is None {
      ignore(context.windows().open("main")) catch {
        error => println("failed to reopen main window: " + error.to_string())
      }
    }
  })
  .run_or_abort()
}

KeepRunning changes last-window behavior, while single_instance routes later launches into the existing process. On Reopen, the callback first checks whether main exists and opens a new instance only when absent. Closing a window and quitting the application are deliberately different operations.

A real background application needs an accessible explicit Quit action, such as a tray command. Stopping the development process demonstrates forced termination, not successful lifecycle cleanup. Reopen obtains a fresh window instance; do not keep the previous handle.

Run

After completing source checkout and runtime setup, run from the repository root:

moon -C examples run 57_background_residency --target native

Limits and interpretation

Closing the window intentionally does not quit. End the process using the platform Quit action or stop the development command. This differs from the default last-window policy.

Release Notes

0.4.0 (Unreleased)

0.4.0 unifies window and browser APIs, shares typed configuration between the application and CLI, and coordinates update installation with application shutdown. This release includes breaking API changes.

Breaking API changes and migration

Area0.3.40.4.0
Window configurationSeparate primary-window setters and add_window(id, title, entry, ...)WindowConfig shared by App(config), main_window(config) and add_window(id, config, ...)
Initial child viewsApp.with_view(...)WindowConfig.views, for primary or secondary windows
Browser operationswindow.browser() and page methods directly on ViewHandlewindow.web_contents() and view.web_contents(), both returning WebContentsHandle
Browser eventsBrowserEvent, ViewEvent, separate subscriptionsWebContentsEvent and on_web_contents_event; use window_id() and optional view_id() for identity
SessionsBrowser-owned handle, SessionHandle.window_id(), WindowSessionErrorApplication-owned handle, context.session(), SessionError; no window_id()
PDFRequest number plus PdfPrinted / PdfPrintResultAsync web_contents.print_to_pdf(path, options?) -> Unit
Configurationapp.load_config(), ProjectConfig, AppMetadataErrorapp.config(@proton.load_config()), typed AppConfig, AppConfigError
Updatesupdate.install(); update.restart()update.download(); context.quit_and_install()
  • Window configuration: move title, size, theme, titlebar_style, traffic_light_position, and entry_* builder settings into WindowConfig. html, url, file, and asset remain shortcuts; live native-window controls remain on WindowHandle. Window registration copies the initial views array, and secondary-window reopening recreates declared children. #417
  • Web contents: WebContentsHandle becomes an opaque common handle rather than an enum to unpack. Navigation, download, and permission callbacks receive it too. Move window zoom and child-page operations to this handle; layout, visibility, z-order and removal stay on the view. Child pages gain printing, downloads and session access, but do not gain the application command bridge. Request completions remain associated with the originating page, and macOS main-page title updates are delivered correctly. #418
  • Session lifetime: cookies, cache, authentication, certificate exceptions and connection operations use the shared application request context. Handles work before windows open and after pages close, including KeepRunning with no windows. Concurrent cookie reads are independent; runtime teardown wakes pending readers and invalidates retained handles. Update exception handling to SessionError. This remains one shared profile, not per-window sessions. #419
  • PDF completion: call print_to_pdf in an async context and handle completion directly; remove request/event correlation. Page closure, renderer termination and application shutdown end outstanding waits. Cancellation stops waiting, not the native print job, so a file may still be produced. #433
  • Typed configuration: @proton.load_config() reads the complete configuration once; fields such as config.frontend.dev_url remain directly accessible. App.config(config) keeps an independent snapshot, and name() / version() read it without reopening files. CLI development overrides are included in the launch snapshot. Packaged proton-package.json now has the complete configuration shape with nested package; rebuild application and packaged metadata together. Rabbita and scaffold defaults advance to 0.16.4, removing the deprecated moonback dependency. #435
  • Update lifecycle: downloading verifies and retains the artifact without replacing files. Installation follows cancelable quit and successful runtime cleanup. Denying quit retains the download; ordinary quit discards it; forced exit supersedes installation. A successful handoff terminates the old process even with exit code zero. quit_and_install() returns on request acceptance, not replacement startup. The extension response is now UpdateDownloaded, without the old changed field. #436

Fixes

  • Cancelling the task running App.run() now cleans up native windows, browsers and helpers during both startup and normal execution. Cancellation still propagates to the caller. #414
  • Linux and macOS packaging correctly copies files and directories whose paths begin with -, preserving executable permissions. #415
  • Update installation retains single-instance ownership while replacing application files. Automatic Windows updates wait for the original process to exit instead of forcibly terminating it. macOS relaunch works when the new version changes its executable name. macOS/Linux applications with an update channel reject delayed starts of the replaced executable. #436

Upgrading

Keep the CLI and application proton_* dependencies on the same version, then run proton_cli cef setup for the matching helper. Installing the new CLI does not migrate existing application code. Rebuild packaged applications to generate the new configuration format.

Rabbita is updated to 0.16.4. Warren remains 0.3.3 and async remains 0.22.4; the CEF version is unchanged. See application APIs, configuration and the complete changelog.

0.3.4 — 2026-09-28

  • Lifecycle: application quit interception and forced exit, application-level window/browser/session events, and focus/visibility controls. Fixes cover forced exit during will_quit, exit(0), child-renderer termination, page recovery, and closed-window collection.
  • Communication: preserve command error codes, separate command failures from runtime failures, and remove the global pending-request limit. Extension operation inventories now come from bindings.
  • Browser APIs: navigation history and inserted CSS controls; task metrics distinguish CEF tasks from processes.
  • Platforms and packaging: Windows desktop media consent and child-view geometry fixes; macOS query autorelease pools and alert layout; recoverable packaging locks, explicit minimum macOS version metadata, and executable Linux helpers in AppImage staging.
  • CLI: explicit new options no longer prompt again. Async is updated to 0.22.4.

Upgrade notes

Keep the CLI and application proton_* modules on 0.3.4 and run proton_cli cef setup for the matching helper. Reinstalling the CLI does not rewrite existing projects.

The briefly introduced app_metrics API is now task_metrics with AppTaskMetric: task identity is not process identity, and process_cpu_percent / process_memory_bytes can repeat across tasks. Do not sum task rows as application resource usage. See PR #412. Unused legacy command configuration APIs were removed in PR #408; application command bindings remain the supported route.

0.3.3 — 2026-09-21

  • View lifecycle: removed and explicitly closed child views retain the identity needed to deliver their terminal close event. Coverage includes views closed before browser creation and stale events after replacement. PR #378
  • Dependencies: async 0.22.1, x 0.5.5, lexer/parser/moon_config 0.4.0, Rabbita 0.16.2 and Warren 0.3.3. Scaffold defaults and documentation now use these versions. Cancellation handling and protected cleanup follow async's new semantics. PR #379

Updating an application

Use 0.3.3 consistently for the application's proton_* dependencies and CLI. Existing projects retain their configuration and source; installing a new CLI does not rewrite them. Install Warren 0.3.3 when using the isomorphic frontend, and run proton_cli cef setup for the matching helper before building.

Applications that directly use async should review its 0.22 cancellation behavior: cancellation is no longer caught as an ordinary error. A cancelled task's wait() can report TaskCancelled; child process shutdown can complete with an exit result after reaping.

Earlier releases

0.3.2 changes and repository history contain earlier changes.