///|
/// Creates an inline HTML application.
pub fn html(
title : String,
html : String,
width~ : Int32 = 900,
height~ : Int32 = 700,
debug~ : Bool = false,
resizable~ : Bool = true,
) -> App {
App(
WindowConfig(
title,
@manifest.AppEntry::Html(html),
width~,
height~,
size_hint=if resizable { Unconstrained } else { Fixed },
),
)..debug(enabled=debug)
}
///|
/// Creates an inline URL application.
pub fn url(
title : String,
url : String,
width~ : Int32 = 900,
height~ : Int32 = 700,
debug~ : Bool = false,
resizable~ : Bool = true,
) -> App {
App(
WindowConfig(
title,
@manifest.AppEntry::Url(url),
width~,
height~,
size_hint=if resizable { Unconstrained } else { Fixed },
),
)..debug(enabled=debug)
}
///|
/// Creates an inline file application.
pub fn file(
title : String,
path : String,
width~ : Int32 = 900,
height~ : Int32 = 700,
debug~ : Bool = false,
resizable~ : Bool = true,
) -> App {
App(
WindowConfig(
title,
@manifest.AppEntry::File(path),
width~,
height~,
size_hint=if resizable { Unconstrained } else { Fixed },
),
)..debug(enabled=debug)
}
///|
/// Creates an inline asset application.
pub fn asset(
title : String,
path : String,
width~ : Int32 = 900,
height~ : Int32 = 700,
debug~ : Bool = false,
resizable~ : Bool = true,
) -> App {
App(
WindowConfig(
title,
@manifest.AppEntry::Asset(path),
width~,
height~,
size_hint=if resizable { Unconstrained } else { Fixed },
),
)..debug(enabled=debug)
}
///|
/// Creates a web contents view configuration for `WindowHandle::add_view`,
/// following the Electron `new WebContentsView(options)` model: the view
/// renders `url` inside its owning window's content area at explicit
/// top-left bounds, stacked above the window's main page.
pub fn view(
url : String,
width~ : Int32,
height~ : Int32,
x~ : Int32 = 0,
y~ : Int32 = 0,
visible~ : Bool = true,
z_order~ : Int32 = 0,
background_color? : String,
) -> ViewConfig {
ViewConfig(
width~,
height~,
x~,
y~,
visible~,
z_order~,
initial_url=url,
background_color?,
)
}
///|
/// Enables or disables runtime debug mode.
pub fn App::debug(self : App, enabled~ : Bool = true) -> Unit {
self.debug_level(debug_level_from_bool(enabled))
}
///|
/// Sets the runtime debug level.
pub fn App::debug_level(self : App, debug : Int32) -> Unit {
self.debug_level_value = debug
}
///|
/// Enables or disables off-screen headless rendering for the application.
///
/// Headless mode does not create a native top-level window, so native dialogs,
/// menus, title bars, and other window-bound UI are unavailable. Set
/// `PROTON_HEADLESS=1` to force this mode for automated test runs.
pub fn App::headless(self : App, enabled~ : Bool = true) -> Unit {
self.headless_value = enabled
}
///|
/// Controls when Proton enables Chromium's accessibility tree.
///
/// `Automatic` follows assistive-technology activity where the platform can
/// report it. `AlwaysEnabled` keeps the tree available for the whole runtime.
pub fn App::accessibility(self : App, mode : AccessibilityMode) -> Unit {
self.accessibility_mode_value = mode
}
///|
/// Selects the immutable locale used by this application runtime.
pub fn App::locale(self : App, locale : @locale.Locale) -> Unit {
self.explicit_locale = Some(locale)
}
///|
/// Ensures only one operating-system process owns this application identity.
/// Later processes forward their URL, document, or reopen activation and exit
/// after the primary application loop accepts it. Forwarding has a five-second
/// deadline and raises `AppRunError` if confirmation is unavailable or the
/// primary is stopping. Success does not wait for asynchronous launch handlers.
/// A timeout never starts another owner or terminates the existing process.
pub fn App::single_instance(self : App, enabled~ : Bool = true) -> Unit {
self.single_instance_enabled = enabled
}
///|
/// Uses an isolated persistent browser profile below the application's
/// sessionData directory.
pub fn App::session_partition(self : App, partition : String) -> Unit {
self.session_partition_value = Some(partition.trim().to_owned())
}
///|
/// Configures a startup-wide Chromium proxy. This is read when the native
/// runtime starts and cannot be changed while the app is running.
pub fn App::proxy(self : App, server : String, bypass? : String) -> Unit {
self.proxy_server_value = Some(server.trim().to_owned())
self.proxy_bypass_value = bypass.map(value => value.trim().to_owned())
}
///|
/// Sets the maximum time allowed for the native bridge to become ready.
pub fn App::bridge_startup_timeout_ms(self : App, timeout_ms : Int32) -> Unit {
self.bridge_startup_timeout_value_ms = timeout_ms
}
///|
/// Sets the app-level native menu bar.
pub fn App::menu(self : App, menu : MenuBar) -> Unit {
self.menu = Some(menu)
}
///|
/// Declares an application URL scheme for launch forwarding and protocol-client
/// controls. Package configuration must declare the same scheme so distributed
/// applications contain the required platform metadata.
pub fn App::url_scheme(self : App, scheme : String) -> Unit {
let scheme = scheme.trim().to_owned().to_lower()
if !app_url_scheme_is_valid(scheme) {
self.validation_errors.push(
InvalidSetting(name="url_scheme", message="must be a scheme name"),
)
} else if !self.url_schemes.contains(scheme) {
self.url_schemes.push(scheme)
}
}
///|
/// Cancels browser requests whose URL starts with `url_prefix`.
///
/// Matching is case-sensitive and applies to every browser session created
/// by this application, including secondary windows and web contents views.
pub fn App::web_request_cancel_prefix(self : App, url_prefix : String) -> Unit {
let url_prefix = url_prefix.trim().to_owned()
if url_prefix == "" {
self.validation_errors.push(
InvalidSetting(
name="web_request_cancel_prefix",
message="must not be empty",
),
)
} else if self.web_request_cancel_prefixes.contains(url_prefix) {
self.validation_errors.push(
InvalidSetting(name="web_request_cancel_prefix", message="is duplicated"),
)
} else if self.web_request_cancel_prefixes.length() >= 128 {
self.validation_errors.push(
InvalidSetting(name="web_request_cancel_prefix", message="limit exceeded"),
)
} else {
self.web_request_cancel_prefixes.push(url_prefix)
}
}
///|
/// Redirects matching browser requests to `target_url` before loading.
pub fn App::web_request_redirect_prefix(
self : App,
url_prefix : String,
target_url : String,
) -> Unit {
let url_prefix = url_prefix.trim().to_owned()
let target_url = target_url.trim().to_owned()
if url_prefix == "" || target_url == "" {
self.validation_errors.push(
InvalidSetting(
name="web_request_redirect_prefix",
message="prefix and target URL must not be empty",
),
)
} else if self.web_request_redirect_prefixes.any(item => item.0 == url_prefix) {
self.validation_errors.push(
InvalidSetting(
name="web_request_redirect_prefix",
message="is duplicated",
),
)
} else if self.web_request_redirect_prefixes.length() >= 128 {
self.validation_errors.push(
InvalidSetting(
name="web_request_redirect_prefix",
message="limit exceeded",
),
)
} else {
self.web_request_redirect_prefixes.push((url_prefix, target_url))
}
}
///|
/// Overrides a request header for URLs matching `url_prefix`.
pub fn App::web_request_header_prefix(
self : App,
url_prefix : String,
header_name : String,
header_value : String,
) -> Unit {
let url_prefix = url_prefix.trim().to_owned()
let header_name = header_name.trim().to_owned()
if url_prefix == "" || header_name == "" {
self.validation_errors.push(
InvalidSetting(
name="web_request_header_prefix",
message="prefix and header name must not be empty",
),
)
} else if self.web_request_header_prefixes.any(item => {
item.0 == url_prefix && item.1.equal_ignore_ascii_case(header_name)
}) {
self.validation_errors.push(
InvalidSetting(name="web_request_header_prefix", message="is duplicated"),
)
} else {
self.web_request_header_prefixes.push(
(url_prefix, header_name, header_value),
)
}
}
///|
fn app_url_scheme_is_valid(scheme : String) -> Bool {
guard scheme.length() > 0 else { return false }
for index, char in scheme {
let valid = if index == 0 {
char.is_ascii_alphabetic()
} else {
char.is_ascii_alphabetic() ||
char.is_ascii_digit() ||
char == '+' ||
char == '-' ||
char == '.'
}
guard valid else { return false }
}
true
}
///|
/// Registers a document extension for single-instance launch forwarding.
/// OS registration belongs to the package configuration.
pub fn App::document_extension(self : App, extension : String) -> Unit {
let extension = extension.trim().to_owned().to_lower()
let extension = if extension.has_prefix(".") {
extension.unsafe_substring(start=1, end=extension.length()).to_string()
} else {
extension
}
if extension == "" || extension.contains("/") || extension.contains("\\") {
self.validation_errors.push(
InvalidSetting(
name="document_extension",
message="must be a file extension",
),
)
} else if !self.document_extensions.contains(extension) {
self.document_extensions.push(extension)
}
}
///|
/// Configures the signed update channel used by this application.
/// `public_keys` contains PEM-encoded RSA public-key text, not file paths.
/// Keep private keys outside the application.
pub fn App::update_channel(
self : App,
endpoint : String,
public_keys : Array[String],
check_on_launch~ : Bool = true,
freshness_days~ : Int32 = 30,
) -> Unit {
let endpoint = endpoint.trim().to_owned()
if !endpoint.has_prefix("https://") {
self.validation_errors.push(
InvalidSetting(
name="update_channel.endpoint",
message="must be an https URL",
),
)
} else if public_keys.length() == 0 {
self.validation_errors.push(
InvalidSetting(
name="update_channel.public_keys",
message="must contain at least one trusted key",
),
)
} else if freshness_days <= 0 {
self.validation_errors.push(
InvalidSetting(
name="update_channel.freshness_days",
message="must be positive",
),
)
} else {
self.update_channel = Some(ResolvedUpdateChannel::{
endpoint,
public_keys: public_keys.copy(),
check_on_launch,
freshness_days,
})
}
}
///|
/// Registers an application-level handler for URL, file, and reopen inputs.
/// Registers a handler for an update the channel offers.
///
/// Registering one is what turns the automatic check on: an application that
/// has not said what to do when an update exists is not asked to contact a
/// server on launch. The handler runs after the application is up, on the
/// application task group, and never on the startup path.
///
/// It is called with an update that has already been authenticated and found
/// newer. Nothing has been downloaded and nothing will be until the handler
/// asks for it.
pub fn App::on_update_available(
self : App,
handler : async (PendingUpdate) -> Unit noraise,
) -> Unit {
self.update_handlers.push(handler)
}
///|
pub fn App::on_launch_input(
self : App,
handler : async (ApplicationContext, RuntimeLaunchInput) -> Unit noraise,
) -> Unit {
self.launch_input_handlers.push(handler)
}
///|
/// Registers a handler for changes to system appearance. The handler receives
/// the new snapshot on the application task group. Registration does not call
/// the handler; read `system_appearance()` explicitly for the initial state.
/// Window theme changes do not trigger this event.
pub fn App::on_system_appearance_change(
self : App,
handler : async (SystemAppearance) -> Unit noraise,
) -> Unit {
self.system_appearance_handlers.push(handler)
}
///|
/// Registers a handler for taskbar thumbnail-toolbar clicks.
///
/// The handler runs on the application task group with the window that owns
/// the toolbar and the `id` of the button that was clicked. Electron attaches
/// one callback per button; Proton reports the identifier instead so rebuilding
/// the toolbar cannot silently retarget a click. Windows raises the event;
/// other platforms never do, because the thumbnail toolbar is Windows only.
pub fn App::on_thumbar_button_click(
self : App,
handler : async (WindowHandle, String) -> Unit noraise,
) -> Unit {
self.thumbar_button_handlers.push(handler)
}
///|
/// Chooses whether closing the last window exits the application or leaves it
/// running so a later launch input can open another window.
pub fn App::last_window_closed_policy(
self : App,
policy : LastWindowClosedPolicy,
) -> Unit {
self.last_window_closed_policy_value = policy
}
///|
/// Appends an async observer of coalesced native state changes for all windows.
/// Observers do not delay the source operation and may overlap while suspended.
pub fn App::on_window_event(
self : App,
handler : async (WindowHandle, WindowEvent) -> Unit noraise,
) -> Unit {
self.window_event_handlers.push(handler)
}
///|
/// Observes every window Proton creates, Electron's `browser-window-created`.
///
/// Runs synchronously in registration order after the instance is registered,
/// including windows opened later through `WindowManager::open`. It observes
/// creation, not page readiness; configure policy before creating the window.
pub fn App::on_window_created(
self : App,
handler : (WindowHandle) -> Unit noraise,
) -> Unit {
self.window_created_handlers.push(handler)
}
///|
/// Observes every web contents Proton creates, Electron's
/// `web-contents-created`.
///
/// Both a window's main browser page and each web contents view report here,
/// including views declared in `WindowConfig` and views added later
/// through `WindowHandle::add_view`. Runs synchronously in registration order;
/// it is not a setup hook before initial navigation or a page-ready barrier.
pub fn App::on_web_contents_created(
self : App,
handler : (WebContentsHandle) -> Unit noraise,
) -> Unit {
self.web_contents_created_handlers.push(handler)
}
///|
/// Observes renderer processes that disappear, Electron's
/// `render-process-gone`.
///
/// The handler runs for every affected web contents and reports Electron's
/// reason vocabulary with the platform exit code. Proton does not reload the
/// page automatically; `WebContentsHandle::reload` decides
/// what happens next.
pub fn App::on_render_process_gone(
self : App,
handler : async (WebContentsHandle, RenderProcessGoneDetails) -> Unit noraise,
) -> Unit {
self.render_process_gone_handlers.push(handler)
}
///|
/// Observes the application session Proton created for this run, Electron's
/// `session-created`. Runs synchronously in registration order before
/// application startup hooks. Use `setup` for asynchronous setup.
///
/// Proton runs one session per application, so the handler runs once during
/// startup, before the `setup` start hooks.
pub fn App::on_session_created(
self : App,
handler : (ApplicationSession) -> Unit noraise,
) -> Unit {
self.session_created_handlers.push(handler)
}
///|
/// Observes events for main pages and child views: loading changes, main-frame
/// navigations, title updates, load failures, and page-search results.
pub fn App::on_web_contents_event(
self : App,
handler : async (WebContentsHandle, WebContentsEvent) -> Unit noraise,
) -> Unit {
self.contents_event_handlers.push(handler)
}
///|
/// Reviews downloads before CEF chooses a destination.
/// Replaces the previously configured handler.
pub fn App::set_download_handler(
self : App,
handler : async (WebContentsHandle, DownloadRequest) -> DownloadDecision noraise,
) -> Unit {
self.download_handler = Some(handler)
}
///|
/// Reviews invalid TLS certificates. The default is denial.
/// Replaces the previously configured handler.
pub fn App::set_certificate_error_handler(
self : App,
handler : async (WebContentsHandle, CertificateError) -> BrowserPermissionDecision noraise,
) -> Unit {
self.certificate_handler = Some(handler)
}
///|
/// Reviews camera, microphone, and display-capture requests. The default is
/// denial.
/// Replaces the previously configured handler.
pub fn App::set_media_permission_handler(
self : App,
handler : async (WebContentsHandle, MediaPermissionRequest) -> BrowserPermissionDecision noraise,
) -> Unit {
self.media_handler = Some(handler)
}
///|
/// Observes download progress and terminal states.
pub fn App::on_download_event(
self : App,
handler : async (WebContentsHandle, DownloadEvent) -> Unit noraise,
) -> Unit {
self.download_event_handlers.push(handler)
}
///|
/// Runs before an accepted application quit starts closing windows.
///
/// This is Electron's cancelable `before-quit` step. `Prevent` aborts the quit
/// request, keeps every window open, and leaves the application running; a
/// later quit request runs the handler again.
/// Replaces the previously configured handler.
pub fn App::set_before_quit_handler(
self : App,
handler : async (ApplicationContext) -> ApplicationQuitDecision noraise,
) -> Unit {
self.before_quit_handler = Some(handler)
}
///|
/// Runs after every application window has closed and before Proton
/// terminates.
///
/// This is Electron's cancelable `will-quit` step. `Prevent` aborts the
/// termination and leaves the application running without windows; use
/// `LastWindowClosedPolicy::KeepRunning` semantics to open a window again.
/// Replaces the previously configured handler.
pub fn App::set_will_quit_handler(
self : App,
handler : async (ApplicationContext) -> ApplicationQuitDecision noraise,
) -> Unit {
self.will_quit_handler = Some(handler)
}
///|
/// Observes the final `quit` step with the process exit code.
///
/// Runs synchronously in registration order after exit is committed and before
/// lifecycle shutdown and native cleanup. Register async cleanup with
/// `context.add_cleanup` during `setup`.
/// `exit_code` is requested by `ApplicationContext::quit` or
/// `ApplicationContext::exit`.
pub fn App::on_quit(
self : App,
handler : (ApplicationContext, Int32) -> Unit noraise,
) -> Unit {
self.quit_handlers.push(handler)
}
///|
/// Adds a secondary window owned by the standard application lifecycle.
pub fn App::add_window(
self : App,
id : String,
config : WindowConfig,
open_on_start~ : Bool = true,
) -> Unit {
let normalized_id = id.trim().to_owned()
if normalized_id == "" || normalized_id == "main" {
self.validation_errors.push(
InvalidSetting(
name="window.id",
message=if normalized_id == "main" {
"\"main\" is reserved for the primary window"
} else {
"must not be empty"
},
),
)
} else if self.windows.any(window => window.0 == normalized_id) {
self.validation_errors.push(
InvalidSetting(
name="window.id",
message="duplicate secondary window id: " + normalized_id,
),
)
} else {
self.windows.push((normalized_id, config.snapshot(), open_on_start))
}
}
///|
fn manifest_window_size_hint(hint : WindowSizeHint) -> @manifest.WindowSizeHint {
match hint {
WindowSizeHint::Unconstrained => @manifest.WindowSizeHint::None
WindowSizeHint::Fixed => @manifest.WindowSizeHint::Fixed
WindowSizeHint::Min => @manifest.WindowSizeHint::Min
WindowSizeHint::Max => @manifest.WindowSizeHint::Max
}
}
///|
/// Adds one package registrar for typed application commands.
///
/// Registration runs before any window is created and is sealed before the
/// renderer bridge starts accepting requests. Only the operations registered
/// by this callback are granted to its targets; the primary configured entry is
/// the default target.
pub fn App::commands(
self : App,
register : (@proton_command.CommandRegistrar) -> Unit raise,
targets? : Array[RendererTarget],
) -> Unit {
self.command_capabilities.push(AppCommandCapability::{
register,
targets: renderer_capability_targets(targets),
})
}
///|
/// Appends application setup, awaited in registration order before automatic
/// startup windows open. Unlike observers, setup can fail startup. Register
/// acquired resources immediately with `context.add_cleanup`.
pub fn App::setup(
self : App,
setup : async (ApplicationContext) -> Unit,
) -> Unit {
self.application_setup.push(setup)
}
///|
/// Appends setup awaited in registration order for each window after page
/// readiness, including reopened windows.
/// Register instance-owned resources with `context.add_cleanup`.
pub fn App::setup_window(
self : App,
setup : async (WindowContext) -> Unit,
) -> Unit {
self.window_setup.push(setup)
}
///|
/// Grants one typed extension capability to its renderer targets.
///
/// The default target is the configured entry of the primary window. The
/// capability registers its backend handlers and exposes only its validated
/// scope to those renderers.
pub fn App::capability(
self : App,
capability : @proton_extension.RendererCapability,
targets? : Array[RendererTarget],
) -> Unit {
let id = capability.id()
if id.trim().to_owned() == "" {
self.validation_errors.push(
InvalidSetting(name="capability.id", message="must not be empty"),
)
} else {
let command_spec = Some(capability.command_spec()) catch {
error => {
self.validation_errors.push(
ExtensionAdaptationFailed(extension_id=id, error~),
)
None
}
}
match command_spec {
Some(spec) =>
self.extension_capabilities.push(ExtensionCapabilityBinding::{
definition_id: capability.definition_id(),
spec,
event_source: capability.event_source(),
scope: capability.scope(),
targets: renderer_capability_targets(targets),
})
None => ()
}
}
}
///|
/// Selects a window's configured entry as a renderer capability target.
pub fn RendererTarget::entry(window~ : String = "main") -> RendererTarget {
RendererTarget::{ window, bundled: false, }
}
///|
/// Selects Proton's bundled application origin in one window as a renderer
/// capability target.
pub fn RendererTarget::bundled(window~ : String = "main") -> RendererTarget {
RendererTarget::{ window, bundled: true, }
}
///|
fn renderer_capability_targets(
configured : Array[RendererTarget]?,
) -> Array[RendererTarget] {
match configured {
Some(targets) => targets.copy()
None => [RendererTarget::entry()]
}
}