feat: add app shims on macOS (#54932)

* feat: add app shims on macOS

An app shim is a small app, in a bundle of its own, that shows windows on
behalf of a host Electron app. The host still creates each window, loads
its page and handles its events, but the NSWindow belongs to the shim's
process, so the Dock, the app switcher, Mission Control and Stage Manager
attribute the window to the shim and not to the host.

- `app.startAppShimServer()` / `app.stopAppShimServer()` and the
  `app-shim-connected` event, which hands over an `AppShimHost`.
- A `BaseWindow` / `BrowserWindow` created with the `appShim` option has
  its native window made in that shim's process, through Chromium's
  remote_cocoa. Such a window is a new NativeWindow implementation that
  only goes through views::Widget; what remote_cocoa has no call for is
  a no-op (see docs/api/app-shim-host.md).
- The shim's executable is `Helpers/app_shim_loader` from the framework
  bundle. It links nothing from Electron: it loads the host's framework
  and calls the new `ElectronAppShimMain` export, which runs an
  `app-shim` process that connects back to the host over a Mach
  bootstrap server.
- Electron Framework gains an rpath to the folder it is in, so that it
  finds its sibling frameworks when an executable in another bundle
  loads it.

Not available in Mac App Store builds.

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: app shim menus, dialogs, window chrome and review fixes

Follow-up to the first app shim commit.

Fixes from review:
- A window torn down from one of its own listeners (shim.quit() or
  win.destroy() in `show`) no longer frees the native host that is
  notifying.
- The native close button no longer leaves the shim waiting on an answer
  that was dropped with the window.
- Keys the page does not handle go back to the shim, whose menu they are
  shortcuts of, and not to the host's menu.
- stopAppShimServer() drops shims that have connected and not said hello.
- Quitting a shim asks the host without blocking and without cancelling a
  logout; quit-requested can now be answered with "no".
- Size limits and useContentSize are in content sizes; restore() and
  unmaximize() each do one thing; webContents.isFocused() works.

New:
- shim.setMenu(): a menu bar of the shim's own, built in the shim from a
  description of the Menu, with clicks sent back.
- Message boxes and open and save dialogs are sheets of the window in the
  shim's process.
- titleBarStyle, traffic light position and visibility, vibrancy, tabs and
  Quick Look, applied in the shim to the NSWindow that remote_cocoa made.
- A Chromium patch for what remote_cocoa has no call for: movable,
  closable, minimizable and fullscreenable, clearing an aspect ratio, and
  yielding activation to the hosting process by pid.
- A shim that loaded another build of Electron than the running host
  tells the host and quits: app-shim-version-mismatch.

The API is marked experimental in the docs.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address review of app shims

- A window in an app shim can't be given a parent, at construction or
  with setParentWindow(), as the docs say.
- shim.setMenu() is now shim.setApplicationMenu().
- A message box that is showing under an id makes way for a new one with
  that id, whichever process shows it.
- Mac App Store builds no longer export the shim's entry point; they
  already had no loader.
- setProgressBar() and setHasShadow() are listed among what does nothing
  on a window in a shim.
- The spec no longer asserts inside an event listener.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: clang-tidy findings in app shim code

* docs: reword the app shim docs for clarity

* chore: update patches

Re-exported after rebasing onto Chromium 158.0.8105.0; only line offsets
changed.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: use vitest matchers in the app shim spec

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Samuel AttardandClaude authored and GitHub committed 2026-10-16 14:03:19 -07:00
1 parent d3c6c8495c
commit 0edcdc47c0
53 files changed
+6199 -99

No files matched your search

+27 -1
View File
@@ -684,11 +684,14 @@ source_set("electron_lib") {
if (is_mac) {
deps += [
"shell/common:app_shim",
"//components/os_crypt/async/browser:keychain_key_provider",
"//components/os_crypt/common:keychain_password_mac",
"//components/remote_cocoa/app_shim",
"//components/remote_cocoa/browser",
"//content/browser:mac_helpers",
"//mojo/core/embedder",
"//mojo/public/cpp/platform",
"//ui/accelerated_widget_mac",
]
@@ -1256,6 +1259,20 @@ if (is_mac) {
deps = [ ":electron_swiftshader_binaries" ]
}
# The executable of an app shim. Apps copy it into the shim bundles they make;
# it links nothing from Electron and loads the host app's framework.
executable("electron_app_shim_loader") {
output_name = "app_shim_loader"
sources = [ "shell/app_shim/app_shim_loader_mac.mm" ]
frameworks = [ "Cocoa.framework" ]
}
bundle_data("electron_app_shim_loader_bundle_data") {
sources = [ "$root_out_dir/app_shim_loader" ]
outputs = [ "{{bundle_contents_dir}}/Helpers/{{source_file_part}}" ]
public_deps = [ ":electron_app_shim_loader" ]
}
bundle_data("electron_crashpad_helper") {
sources = [ "$root_out_dir/chrome_crashpad_handler" ]
@@ -1354,7 +1371,10 @@ if (is_mac) {
":electron_xibs",
]
if (!is_mas_build) {
bundle_deps += [ ":electron_crashpad_helper" ]
bundle_deps += [
":electron_app_shim_loader_bundle_data",
":electron_crashpad_helper",
]
}
info_plist = "shell/common/resources/mac/Info.plist"
@@ -1372,6 +1392,12 @@ if (is_mac) {
"-rpath",
"@loader_path/Libraries",
# The folder this framework is in, where its sibling frameworks are. The
# app's executable has it as an rpath too, but an app shim loads this
# framework from an executable in another bundle.
"-rpath",
"@loader_path/../../..",
# Required for exporting all symbols of libuv.
"-Wl,-force_load,obj/third_party/electron_node/deps/uv/libuv.a",
]
+263
View File
@@ -0,0 +1,263 @@
## Class: AppShimHost
> Show windows under a separate app with its own Dock icon.
> [!NOTE]
> App shims are experimental. This API may change in a future Electron
> release.
Process: [Main](../glossary.md#main-process)<br />
_This class is not exported from the `'electron'` module. It is only available as a return value of other methods in the Electron API._
An app shim is a small macOS app, in its own bundle, that displays windows on
behalf of your app. Your app still creates each window, loads its content and
handles its events, but the native window belongs to the shim's process. As a
result, the Dock, the app switcher, Mission Control and Stage Manager show the
window under the shim's name and icon instead of your app's.
An `AppShimHost` represents your app's connection to one running shim. Your
app receives one in the
[`app-shim-connected`](app.md#event-app-shim-connected-macos-experimental)
event after calling
[`app.startAppShimServer()`](app.md#appstartappshimserveroptions-macos-experimental).
```js
const { app, BrowserWindow } = require('electron')
app.whenReady().then(() => {
app.startAppShimServer()
})
app.on('app-shim-connected', (event, shim) => {
// Any process running as the same user can connect, so verify the shim.
if (!shim.satisfiesCodeRequirement('anchor apple generic and certificate leaf[subject.OU] = "ABCDE12345"')) {
shim.quit()
return
}
const win = new BrowserWindow({ appShim: shim })
win.loadURL('https://example.com')
shim.on('activate', (event, hasVisibleWindows) => {
if (!hasVisibleWindows) win.show()
})
})
```
**Creating an app shim**
A shim is an app bundle whose executable is a copy of
`Electron Framework.framework/Helpers/app_shim_loader` from your packaged app.
The loader does not link against Electron. It loads your app's Electron
Framework when it starts, so a shim created by one version of your app keeps
working after your app is updated.
The shim's `Info.plist` holds the usual keys (`CFBundleExecutable`,
`CFBundleIdentifier`, `CFBundleName`, `CFBundleIconFile` and so on) as well as these keys:
* `ElectronAppShimHostPath` string - The absolute path of your app's `.app`
bundle.
* `ElectronAppShimHostBundleIdentifier` string - Your app's bundle identifier.
It is used to locate your app if `ElectronAppShimHostPath` is missing or no
longer points to an app.
* `ElectronAppShimServerName` string (optional) - The `name` your app passes
to `app.startAppShimServer()`, if it passes one.
* `ElectronAppShimHostArguments` string[] (optional) - Command-line arguments
used to launch your app if it is not running when the shim starts.
When a shim starts, it connects to your app. If your app is not running, the
shim launches it in the background and waits up to one minute for it to call
`app.startAppShimServer()`.
In a signed app, macOS only allows the shim to load your framework if both are
signed by the same team.
**App updates**
A shim can only connect to your app while your app is running the same version
that is installed on disk. If your app's bundle is replaced while your app is
running, any shim launched afterwards quits immediately without showing any
UI, and your app receives an
[`app-shim-version-mismatch`](app.md#event-app-shim-version-mismatch-macos-experimental)
event. Use that event to ask the user to restart your app.
Updates installed by [`autoUpdater`](auto-updater.md) are applied after your
app quits, so they do not cause a mismatch.
**Limitations**
App shims are not available in Mac App Store builds.
A window in an app shim has no native window in your app's process. Window
APIs that operate directly on the native window are not supported yet and do
nothing:
* The Touch Bar
* The window controls overlay (`titleBarOverlay`)
* Simple fullscreen
* `win.setEnabled()`
* `win.flashFrame()` (use [`shim.bounce()`](#shimbouncetype-experimental) instead)
* `win.setProgressBar()`
* `win.setHasShadow()`
* `win.setRepresentedFilename()`
* `win.setDocumentEdited()`
* `win.setHiddenInMissionControl()`
`win.getNativeWindowHandle()` and `win.getMediaSourceId()` refer to a hidden
placeholder window in your app's process, not to the window on screen.
Other differences from a regular window:
* A window's shim is set when the window is created and cannot be changed.
* A window in a shim cannot be `modal`, cannot have a `parent` and cannot be
the `parent` of another window. Attempting any of these throws an error.
* `win.addTabbedWindow()` only works between two windows in the same shim.
* Window operations are sent to the shim's process and applied asynchronously,
so window state may not update immediately. For example, after calling
`win.show()`, `win.isVisible()` returns `false` until the window emits its
`show` event.
* Message boxes and open and save dialogs from the [`dialog`](dialog.md)
module can use a window in a shim as their parent. The shim displays them
as sheets attached to that window. The synchronous variants, such as
`dialog.showMessageBoxSync()`, do not block your app while the dialog is
open, so your app may handle other events before they return. Other dialogs
are shown without a parent window.
* While a shim is the active app, macOS shows the shim's menu bar instead of
your app's. Set it with
[`shim.setApplicationMenu()`](#shimsetapplicationmenumenu-experimental). If
no menu is set, the shim shows a minimal default menu. Your app's
application menu and its accelerators do not apply to windows in a shim.
### Instance Events
#### Event: 'activate' _Experimental_
Returns:
* `event` Event
* `hasVisibleWindows` boolean
Emitted when the user clicks the shim's Dock icon, or launches the shim again
while it is already running.
#### Event: 'open-file' _Experimental_
Returns:
* `event` Event
* `path` string
Emitted when the user opens a file with the shim.
#### Event: 'open-url' _Experimental_
Returns:
* `event` Event
* `url` string
Emitted when the user opens a URL with the shim.
#### Event: 'quit-requested' _Experimental_
Returns:
* `event` Event
Emitted when the shim is asked to quit: by the user, for example with
<kbd>Cmd</kbd>+<kbd>Q</kbd> or from its Dock menu, or by macOS at logout.
By default, the shim quits and its windows are closed without emitting their
`close` events. To keep the shim running, call `event.preventDefault()`. You
can then close its windows yourself and call `shim.quit()` when you are done.
`event.preventDefault()` must be called synchronously in the listener. If your
app's main process is unresponsive for more than five seconds, the shim quits
without waiting for it.
#### Event: 'disconnected' _Experimental_
Returns:
* `event` Event
Emitted when the shim's process has exited or its connection was lost. All
windows that were created in the shim have already been closed.
### Instance Methods
#### `shim.isConnected()` _Experimental_
Returns `boolean` - Whether the shim's process is still connected.
#### `shim.satisfiesCodeRequirement(requirement)` _Experimental_
* `requirement` string - A [code requirement](https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/RequirementLang/RequirementLang.html).
Returns `boolean` - Whether the code signature of the shim's process satisfies
`requirement`. The check is performed on the running process that connected,
not on the bundle at `shim.bundlePath`.
Throws if `requirement` cannot be parsed.
#### `shim.setBadge(text)` _Experimental_
* `text` string
Sets the text displayed on the shim's Dock icon. An empty string clears it.
#### `shim.bounce([type])` _Experimental_
* `type` string (optional) - Can be `critical` or `informational`. The default is
`informational`
Bounces the shim's Dock icon, in the same way that
[`dock.bounce()`](dock.md#dockbouncetype-macos) bounces your app's.
#### `shim.cancelBounce()` _Experimental_
Cancels a bounce started by `shim.bounce()`.
#### `shim.setApplicationMenu(menu)` _Experimental_
* `menu` [Menu](menu.md) | null
Sets the menu bar that is shown while the shim is the active app. This is the
shim's equivalent of
[`Menu.setApplicationMenu()`](menu.md#menusetapplicationmenumenu). Passing
`null` restores the shim's minimal default menu.
The `click` handlers of the menu's items and the menu's `menu-will-show` event
run in your app's main process. Items with a `role` act on the shim instead of
your app: for example, the `quit` role quits the shim. The default labels of
the `about`, `hide` and `quit` roles use the shim's name.
The shim displays the menu's items as they were when this method was called.
Later changes to an item's `label`, `enabled`, `checked` or `visible`
properties are applied when the shim becomes the active app, when one of its
menus is opened, and after an item is clicked.
#### `shim.quit()` _Experimental_
Quits the shim's process and closes its windows. The windows do not emit
`close` events. They emit `closed` shortly after this method returns.
`shim.isConnected()` returns `false` as soon as this method returns.
### Instance Properties
#### `shim.pid` _Readonly_ _Experimental_
A `number` representing the process ID of the shim.
#### `shim.bundlePath` _Readonly_ _Experimental_
A `string` representing the path of the app bundle that the shim's process was
launched from.
#### `shim.bundleId` _Readonly_ _Experimental_
A `string` representing the bundle identifier from that bundle's `Info.plist`.
This value is not verified, so do not use it to decide whether to trust the
shim. Use
[`shim.satisfiesCodeRequirement()`](#shimsatisfiescoderequirementrequirement-experimental)
instead.
+70
View File
@@ -153,6 +153,52 @@ this event, such as launching the application for the first time, attempting
to re-launch the application when it's already running, or clicking on the
application's dock or taskbar icon.
### Event: 'app-shim-connected' _macOS_ _Experimental_
Returns:
* `event` Event
* `shim` [AppShimHost](app-shim-host.md)
Emitted when an app shim connects to your app. Shims can only connect after
[`app.startAppShimServer()`](#appstartappshimserveroptions-macos-experimental)
has been called.
Any process running as the same user can connect. Verify the shim before
creating windows in it, for example with
[`shim.satisfiesCodeRequirement()`](app-shim-host.md#shimsatisfiescoderequirementrequirement-experimental),
and call `shim.quit()` to reject a shim that you do not trust.
### Event: 'app-shim-version-mismatch' _macOS_ _Experimental_
Returns:
* `event` Event
* `bundlePath` string - The path of the app shim's bundle.
Emitted when an app shim was launched but could not connect because your app's
bundle has been replaced by a different version since your app started. A shim
loads the version of Electron that is on disk, which cannot communicate with
the version that is still running.
The shim quits without showing any UI, so use this event to tell the user what
happened. Shims can connect again after your app restarts.
```js
const { app, dialog } = require('electron')
app.on('app-shim-version-mismatch', async () => {
const { response } = await dialog.showMessageBox({
message: `Restart ${app.name} to finish updating`,
buttons: ['Restart', 'Later']
})
if (response === 0) {
app.relaunch()
app.quit()
}
})
```
### Event: 'did-become-active' _macOS_
Returns:
@@ -1250,6 +1296,30 @@ Activation policy types:
* 'accessory' - The application doesn’t appear in the Dock and doesn’t have a menu bar, but it may be activated programmatically or by clicking on one of its windows.
* 'prohibited' - The application doesn’t appear in the Dock and may not create windows or be activated.
### `app.startAppShimServer([options])` _macOS_ _Experimental_
* `options` Object (optional)
* `name` string (optional) - A name for this server, which lets several
running copies of your app each have their own. It can only contain
letters, digits, `.`, `-` and `_`, and must match the
`ElectronAppShimServerName` of the shims that connect to it. Default is
`appshim`.
Starts accepting connections from [app shims](app-shim-host.md). An
[`app-shim-connected`](#event-app-shim-connected-macos-experimental) event is
emitted for each shim that connects.
Throws if the server cannot be started, for example because another copy of
your app has already started a server with the same `name`.
This method can only be called after the app is ready. It is not available in
Mac App Store builds.
### `app.stopAppShimServer()` _macOS_ _Experimental_
Stops accepting connections from app shims. Shims that have already connected
stay connected.
### `app.importCertificate(options, callback)` _Linux_
* `options` Object
@@ -55,6 +55,10 @@
* `parent` BaseWindow (optional) - Specify parent window. Default is `null`.
* `modal` boolean (optional) - Whether this is a modal window. This only works when the
window is a child window. Default is `false`.
* `appShim` [AppShimHost](../app-shim-host.md) (optional) _macOS_ _Experimental_ - Creates the native window in
the process of this app shim, so that it belongs to the shim's Dock icon and
not to your app's. See [AppShimHost](../app-shim-host.md) for what affordances
are granted to the window.
* `acceptFirstMouse` boolean (optional) _macOS_ - Whether clicking an
inactive window will also click through to the web contents. Default is
`false` on macOS. This option is not configurable on other platforms.
+1
View File
@@ -1,6 +1,7 @@
# THIS FILE IS AUTO-GENERATED, PLEASE DO NOT EDIT BY HAND
auto_filenames = {
api_docs = [
"docs/api/app-shim-host.md",
"docs/api/app.md",
"docs/api/auto-updater.md",
"docs/api/base-window.md",
+20
View File
@@ -125,9 +125,21 @@ filenames = {
"shell/app/electron_main_delegate_mac.mm",
"shell/app/electron_prefetch_mac.h",
"shell/app/electron_prefetch_mac.mm",
"shell/app_shim/app_shim_application.h",
"shell/app_shim/app_shim_application.mm",
"shell/app_shim/app_shim_controller.h",
"shell/app_shim/app_shim_controller.mm",
"shell/app_shim/app_shim_main_delegate.h",
"shell/app_shim/app_shim_main_delegate.mm",
"shell/app_shim/app_shim_menu.h",
"shell/app_shim/app_shim_menu.mm",
"shell/app_shim/app_shim_window.h",
"shell/app_shim/app_shim_window.mm",
"shell/browser/animation_util.h",
"shell/browser/animation_util_mac.mm",
"shell/browser/api/electron_api_app_mac.mm",
"shell/browser/api/electron_api_app_shim_host.h",
"shell/browser/api/electron_api_app_shim_host.mm",
"shell/browser/api/electron_api_base_window_mac.mm",
"shell/browser/api/electron_api_clipboard_mac.mm",
"shell/browser/api/electron_api_menu_mac.h",
@@ -137,6 +149,8 @@ filenames = {
"shell/browser/api/electron_api_push_notifications_mac.mm",
"shell/browser/api/electron_api_system_preferences_mac.mm",
"shell/browser/api/electron_api_web_contents_mac.mm",
"shell/browser/app_shim/app_shim_listener.h",
"shell/browser/app_shim/app_shim_listener.mm",
"shell/browser/auto_updater_mac.mm",
"shell/browser/browser_mac.mm",
"shell/browser/electron_browser_main_parts_mac.mm",
@@ -154,6 +168,8 @@ filenames = {
"shell/browser/mac/in_app_purchase.mm",
"shell/browser/native_window_mac.h",
"shell/browser/native_window_mac.mm",
"shell/browser/native_window_mac_remote.h",
"shell/browser/native_window_mac_remote.mm",
"shell/browser/notifications/mac/cocoa_notification.h",
"shell/browser/notifications/mac/cocoa_notification.mm",
"shell/browser/notifications/mac/notification_center_delegate.h",
@@ -163,6 +179,8 @@ filenames = {
"shell/browser/osr/osr_host_display_client_mac.mm",
"shell/browser/osr/osr_web_contents_view_mac.mm",
"shell/browser/relauncher_mac.cc",
"shell/browser/ui/views/content_view_targeter_delegate.cc",
"shell/browser/ui/views/content_view_targeter_delegate.h",
"shell/browser/ui/certificate_trust_mac.mm",
"shell/browser/ui/certificate_viewer_mac.mm",
"shell/browser/ui/cocoa/electron_bundle_mover.h",
@@ -200,6 +218,8 @@ filenames = {
"shell/browser/webauthn/electron_platform_passkeys_authenticator_mac.mm",
"shell/browser/webauthn/electron_platform_passkeys_discovery.h",
"shell/browser/webauthn/electron_platform_passkeys_discovery_mac.mm",
"shell/common/app_shim_constants.cc",
"shell/common/app_shim_constants.h",
"shell/common/api/electron_api_native_image_mac.mm",
"shell/common/asar/archive_mac.mm",
"shell/common/asar/integrity_digest.h",
+1
View File
@@ -138,3 +138,4 @@ fix_gate_webstoreprivate_on_a_delegated_availability_check.patch
build_drop_the_unused_chrome_browser_win_dep_from_color_mixers.patch
fix_plumb_the_requesting_frame_through_pointer_lock_requests.patch
fix_avoid_misaligned_char16_t_reads_in_crdtp_string_deserialization.patch
feat_let_the_host_of_a_remote_nswindow_set_more_of_its_behavior.patch
@@ -0,0 +1,248 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Samuel Attard <sattard@anthropic.com>
Date: Wed, 14 Oct 2026 10:58:22 -0700
Subject: feat: let the host of a remote NSWindow set more of its behavior
Electron's app shims create their windows through remote_cocoa, where the
host process has no NSWindow to call. This adds what BaseWindow's API
needs and remote_cocoa has no call for:
- NativeWidgetNSWindow.SetMovable, SetClosable and SetMiniaturizable.
- NativeWidgetNSWindow.SetFullscreenable, which decides whether the
window can go full screen whatever the size constraints say. Without it
that follows from "resizable and maximizable", which is a different
question to an app that sets the three separately.
- An empty aspect ratio passed to SetAspectRatio removes the restriction,
where it used to hit a DCHECK.
- ApplicationHost::set_pid. When the embedder knows the process that hosts
the application, showing one of its windows yields activation to that
process and not to whichever running app declares the bundle
identifier, which the hosted process chooses for itself.
Chromium's own remote windows (PWA shims) call none of these, so there is
nothing here that it needs; the calls could be upstreamed as they are if
it ever does.
Co-Authored-By: Claude <noreply@anthropic.com>
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
index de4c147140265a8a576aa5ef4d0276e1a3cf1de5..c5bb4f6871c161e457d25a229005285e5f70e660 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h
@@ -320,6 +320,10 @@ class REMOTE_COCOA_APP_SHIM_EXPORT NativeWidgetNSWindowBridge
void SetCALayerParams(gfx::CALayerParams ca_layer_params) override;
void SetWindowTitle(const std::u16string& title) override;
void SetIgnoresMouseEvents(bool ignores_mouse_events) override;
+ void SetMovable(bool movable) override;
+ void SetClosable(bool closable) override;
+ void SetMiniaturizable(bool miniaturizable) override;
+ void SetFullscreenable(bool fullscreenable) override;
void MakeFirstResponder() override;
void SortSubviews(
const std::vector<uint64_t>& associated_subview_ids) override;
@@ -471,6 +475,10 @@ class REMOTE_COCOA_APP_SHIM_EXPORT NativeWidgetNSWindowBridge
std::unique_ptr<CocoaWindowMoveLoop> window_move_loop_;
ui::mojom::ModalType modal_type_ = ui::mojom::ModalType::kNone;
bool is_translucent_window_ = false;
+ // Set by SetFullscreenable(); takes the place of what the size constraints
+ // would decide.
+ std::optional<bool> fullscreenable_override_;
+ void ApplyFullscreenableOverride();
id __strong local_event_monitor_;
raw_ptr<NativeWidgetNSWindowBridge> parent_ =
diff --git a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
index df23bb8f408e9a712ceeb24558947a83ce62e634..3c364d2f4c2bd2bc72efa22183992510ab75785a 100644
--- a/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
+++ b/components/remote_cocoa/app_shim/native_widget_ns_window_bridge.mm
@@ -1628,6 +1628,7 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
gfx::ApplyNSWindowSizeConstraints(window_, min_size, max_size,
shows_resize_controls,
shows_fullscreen_controls);
+ ApplyFullscreenableOverride();
}
void NativeWidgetNSWindowBridge::OnShowAnimationComplete() {
@@ -2043,7 +2044,10 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
void NativeWidgetNSWindowBridge::SetAspectRatio(
const gfx::SizeF& aspect_ratio,
const gfx::Size& excluded_margin) {
- DCHECK(!aspect_ratio.IsEmpty());
+ if (aspect_ratio.IsEmpty()) {
+ [window_delegate_ clearAspectRatio];
+ return;
+ }
[window_delegate_ setAspectRatio:aspect_ratio.width() / aspect_ratio.height()
excludedMargin:excluded_margin];
}
@@ -2123,6 +2127,56 @@ NSUInteger CountBridgedWindows(NSArray* child_windows) {
[window_ setIgnoresMouseEvents:ignores_mouse_events];
}
+void NativeWidgetNSWindowBridge::SetMovable(bool movable) {
+ [window_ setMovable:movable];
+}
+
+void NativeWidgetNSWindowBridge::SetClosable(bool closable) {
+ NSUInteger style_mask = [window_ styleMask];
+ if (closable) {
+ style_mask |= NSWindowStyleMaskClosable;
+ } else {
+ style_mask &= ~NSWindowStyleMaskClosable;
+ }
+ [window_ setStyleMask:style_mask];
+}
+
+void NativeWidgetNSWindowBridge::SetMiniaturizable(bool miniaturizable) {
+ NSUInteger style_mask = [window_ styleMask];
+ if (miniaturizable) {
+ style_mask |= NSWindowStyleMaskMiniaturizable;
+ } else {
+ style_mask &= ~NSWindowStyleMaskMiniaturizable;
+ }
+ [window_ setStyleMask:style_mask];
+}
+
+void NativeWidgetNSWindowBridge::SetFullscreenable(bool fullscreenable) {
+ fullscreenable_override_ = fullscreenable;
+ // Not while in full screen or on the way: the window would lose the
+ // behavior it needs to come back. The next size constraints apply it.
+ if (fullscreen_controller_.CanResize()) {
+ ApplyFullscreenableOverride();
+ }
+}
+
+void NativeWidgetNSWindowBridge::ApplyFullscreenableOverride() {
+ if (!fullscreenable_override_) {
+ return;
+ }
+ // A resizable window can go full screen unless it is marked as auxiliary,
+ // so clearing the primary behavior alone would not stop it.
+ NSWindowCollectionBehavior behavior = [window_ collectionBehavior];
+ if (*fullscreenable_override_) {
+ behavior |= NSWindowCollectionBehaviorFullScreenPrimary;
+ behavior &= ~NSWindowCollectionBehaviorFullScreenAuxiliary;
+ } else {
+ behavior &= ~NSWindowCollectionBehaviorFullScreenPrimary;
+ behavior |= NSWindowCollectionBehaviorFullScreenAuxiliary;
+ }
+ [window_ setCollectionBehavior:behavior];
+}
+
void NativeWidgetNSWindowBridge::MakeFirstResponder() {
[window_ makeFirstResponder:bridged_view_];
}
diff --git a/components/remote_cocoa/app_shim/views_nswindow_delegate.h b/components/remote_cocoa/app_shim/views_nswindow_delegate.h
index 40b1e27ecca25715dd6677743c9f520576cd657b..751d8af8b8f03c1de9f1f1b143057fe6b3e7648d 100644
--- a/components/remote_cocoa/app_shim/views_nswindow_delegate.h
+++ b/components/remote_cocoa/app_shim/views_nswindow_delegate.h
@@ -52,6 +52,9 @@ REMOTE_COCOA_APP_SHIM_EXPORT
- (void)setAspectRatio:(float)aspectRatio
excludedMargin:(const gfx::Size&)excludedMargin;
+// Removes the restriction set by setAspectRatio:excludedMargin:.
+- (void)clearAspectRatio;
+
@end
#endif // COMPONENTS_REMOTE_COCOA_APP_SHIM_VIEWS_NSWINDOW_DELEGATE_H_
diff --git a/components/remote_cocoa/app_shim/views_nswindow_delegate.mm b/components/remote_cocoa/app_shim/views_nswindow_delegate.mm
index 1aa3983953a5e292fd57a9e4a860a49c6942f7e5..f2c394a8afe4a09ab473e000938c42dfe0e2ea42 100644
--- a/components/remote_cocoa/app_shim/views_nswindow_delegate.mm
+++ b/components/remote_cocoa/app_shim/views_nswindow_delegate.mm
@@ -153,6 +153,11 @@ - (void)setAspectRatio:(float)aspectRatio
_excludedMargin = excludedMargin;
}
+- (void)clearAspectRatio {
+ _aspectRatio.reset();
+ _excludedMargin = gfx::Size();
+}
+
- (NSSize)windowWillResize:(NSWindow*)window toSize:(NSSize)size {
const NSRect windowFrame = [window frame];
if (_aspectRatio) {
diff --git a/components/remote_cocoa/browser/application_host.h b/components/remote_cocoa/browser/application_host.h
index c4f1bd2e8d646306ae2c80100aba01085777a0fc..d5d4d778fb1190eb22b4933632ad9d3a44c83141 100644
--- a/components/remote_cocoa/browser/application_host.h
+++ b/components/remote_cocoa/browser/application_host.h
@@ -7,6 +7,7 @@
#include "base/observer_list.h"
#include "base/observer_list_types.h"
+#include "base/process/process_handle.h"
#include "components/remote_cocoa/browser/remote_cocoa_browser_export.h"
#include "components/remote_cocoa/common/application.mojom.h"
#include "mojo/public/cpp/bindings/associated_remote.h"
@@ -42,9 +43,16 @@ class REMOTE_COCOA_BROWSER_EXPORT ApplicationHost {
const std::string& bundle_id() const { return bundle_id_; }
+ // The process that hosts the application, when the embedder knows it.
+ // Activation is then yielded to that process, and not to whichever running
+ // app declares |bundle_id|.
+ void set_pid(base::ProcessId pid) { pid_ = pid; }
+ base::ProcessId pid() const { return pid_; }
+
private:
mojo::AssociatedRemote<mojom::Application> application_remote_;
const std::string bundle_id_;
+ base::ProcessId pid_ = base::kNullProcessId;
base::ObserverList<Observer> observers_;
};
diff --git a/components/remote_cocoa/common/native_widget_ns_window.mojom b/components/remote_cocoa/common/native_widget_ns_window.mojom
index 8ca5673a225b69b5630383ed9673653ba03a0e97..f650180f02ef8c5c92421a5bec35f1b13355e533 100644
--- a/components/remote_cocoa/common/native_widget_ns_window.mojom
+++ b/components/remote_cocoa/common/native_widget_ns_window.mojom
@@ -291,6 +291,7 @@ interface NativeWidgetNSWindow {
// Presumably, (10, 15) of this area would be used to draw client-drawn
// decorations, while the remaining (30, 30) would be used for whatever
// content is supposed to have a 1.f aspect-ratio.
+ // An empty |aspect_ratio| removes the restriction.
SetAspectRatio(
gfx.mojom.SizeF aspect_ratio, gfx.mojom.Size excluded_margin);
@@ -303,6 +304,18 @@ interface NativeWidgetNSWindow {
// Ignore or accept mouse events on the NSWindow.
SetIgnoresMouseEvents(bool ignores_mouse_events);
+ // Set whether the window can be moved by dragging it.
+ SetMovable(bool movable);
+
+ // Set whether the window has a close button and a minimize button that
+ // work.
+ SetClosable(bool closable);
+ SetMiniaturizable(bool miniaturizable);
+
+ // Set whether the window can go full screen, whatever the size constraints
+ // say; without this call a window that can resize and maximize can.
+ SetFullscreenable(bool fullscreenable);
+
// Make the content view be the first responder for the NSWindow.
MakeFirstResponder();
diff --git a/ui/views/cocoa/native_widget_mac_ns_window_host.mm b/ui/views/cocoa/native_widget_mac_ns_window_host.mm
index 0d796595adc97607f9c84990123d8a60a84d9ec8..58b73d061e17ce46c6a714e42ad9ed0190f2d328 100644
--- a/ui/views/cocoa/native_widget_mac_ns_window_host.mm
+++ b/ui/views/cocoa/native_widget_mac_ns_window_host.mm
@@ -907,8 +907,17 @@ void HandleAccelerator(const ui::Accelerator& accelerator,
if (@available(macOS 14, *)) {
if (application_host_ &&
new_state == WindowVisibilityState::kShowAndActivateWindow) {
- [NSApp yieldActivationToApplicationWithBundleIdentifier:
- base::SysUTF8ToNSString(application_host_->bundle_id())];
+ NSRunningApplication* application = nil;
+ if (application_host_->pid() != base::kNullProcessId) {
+ application = [NSRunningApplication
+ runningApplicationWithProcessIdentifier:application_host_->pid()];
+ }
+ if (application) {
+ [NSApp yieldActivationToApplication:application];
+ } else {
+ [NSApp yieldActivationToApplicationWithBundleIdentifier:
+ base::SysUTF8ToNSString(application_host_->bundle_id())];
+ }
}
}
@@ -10,6 +10,7 @@ Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/app_shim_loader
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/chrome_crashpad_handler
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib
@@ -10,6 +10,7 @@ Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Electron Framework
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/app_shim_loader
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Helpers/chrome_crashpad_handler
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/
Electron.app/Contents/Frameworks/Electron Framework.framework/Versions/A/Libraries/libffmpeg.dylib
+9
View File
@@ -6,6 +6,7 @@
#define ELECTRON_SHELL_APP_ELECTRON_LIBRARY_MAIN_H_
#include "build/build_config.h"
#include "electron/mas.h"
#if BUILDFLAG(IS_MAC)
extern "C" {
@@ -13,6 +14,14 @@ __attribute__((visibility("default"))) int ElectronMain(int argc, char* argv[]);
__attribute__((visibility("default"))) int ElectronInitializeICUandStartNode(
int argc,
char* argv[]);
#if !IS_MAS_BUILD()
// The entry point of an app shim: a small app, in a bundle of its own, that
// loads this framework from a host Electron app and shows windows on the
// host's behalf. See docs/api/app-shim-host.md. Mac App Store builds have
// neither this nor the loader that calls it.
__attribute__((visibility("default"))) int ElectronAppShimMain(int argc,
char* argv[]);
#endif
}
#endif
+54
View File
@@ -7,18 +7,26 @@
#include "shell/app/electron_library_main.h"
#include <dlfcn.h>
#include "base/apple/bundle_locations.h"
#include "base/apple/scoped_nsautorelease_pool.h"
#include "base/at_exit.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/i18n/icu_util.h"
#include "base/notreached.h"
#include "content/public/app/content_main.h"
#include "electron/fuses.h"
#include "electron/mas.h"
#include "shell/app/electron_main_delegate.h"
#include "shell/app/electron_prefetch_mac.h"
#include "shell/app/node_main.h"
#include "shell/app_shim/app_shim_main_delegate.h"
#include "shell/common/app_shim_constants.h"
#include "shell/common/electron_command_line.h"
#include "shell/common/mac/main_application_bundle.h"
#include "shell/common/uv_includes.h"
@@ -34,6 +42,29 @@ bool HasProcessTypeSwitch(int argc, char* argv[]) {
return false;
}
#if !IS_MAS_BUILD()
// The .app that holds the framework this code was loaded from: the app shim's
// host. Empty when the framework is not inside an app bundle.
base::FilePath HostBundlePathFromLoadedFramework() {
Dl_info info;
if (!dladdr(reinterpret_cast<const void*>(&ElectronAppShimMain), &info) ||
!info.dli_fname) {
return {};
}
base::FilePath path =
base::MakeAbsoluteFilePath(base::FilePath(info.dli_fname));
// <host>.app/Contents/Frameworks/<name>.framework/Versions/A/<name>
while (!path.empty() && path != path.DirName() &&
path.Extension() != ".framework") {
path = path.DirName();
}
if (path.Extension() != ".framework")
return {};
path = path.DirName().DirName().DirName();
return path.Extension() == ".app" ? path : base::FilePath();
}
#endif // !IS_MAS_BUILD()
} // namespace
int ElectronMain(int argc, char* argv[]) {
@@ -76,3 +107,26 @@ int ElectronInitializeICUandStartNode(int argc, char* argv[]) {
base::i18n::InitializeICU();
return electron::NodeMain();
}
#if !IS_MAS_BUILD()
int ElectronAppShimMain(int argc, char* argv[]) {
const base::FilePath host_bundle_path = HostBundlePathFromLoadedFramework();
CHECK(!host_bundle_path.empty())
<< "An app shim must load the framework from inside its host app";
// The main bundle is the shim's; resources come from the host's.
base::apple::SetOverrideOuterBundlePath(host_bundle_path);
base::apple::SetOverrideFrameworkBundlePath(
host_bundle_path.Append("Contents")
.Append("Frameworks")
.Append(ELECTRON_PRODUCT_NAME " Framework.framework"));
base::CommandLine::Init(argc, argv);
base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
"type", electron::app_shim::kProcessType);
electron::AppShimMainDelegate delegate(host_bundle_path);
content::ContentMainParams params{&delegate};
return content::ContentMain(std::move(params));
}
#endif // !IS_MAS_BUILD()
+19
View File
@@ -0,0 +1,19 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_APP_SHIM_APP_SHIM_APPLICATION_H_
#define ELECTRON_SHELL_APP_SHIM_APP_SHIM_APPLICATION_H_
#import <Cocoa/Cocoa.h>
#import "components/remote_cocoa/app_shim/cr_application.h"
// The NSApplication of an app shim process. It notices assistive technologies
// (VoiceOver, Accessibility Inspector, UI scripting) and has the host app turn
// accessibility on, because the accessibility tree of the windows shown here
// is built there.
@interface ElectronAppShimApplication : CrApplication
@end
#endif // ELECTRON_SHELL_APP_SHIM_APP_SHIM_APPLICATION_H_
+81
View File
@@ -0,0 +1,81 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#import "shell/app_shim/app_shim_application.h"
#include "shell/app_shim/app_shim_controller.h"
namespace {
// A screen reader sets AXEnhancedUserInterface as it starts, and other apps
// set and clear it in passing; only one that keeps it set gets web content.
constexpr NSTimeInterval kCompleteModeDelay = 2.0;
void EnableAccessibilitySupport(
electron::mojom::AppShimAccessibilityMode mode) {
if (auto* controller = electron::AppShimController::Get())
controller->EnableAccessibilitySupport(mode);
}
} // namespace
@implementation ElectronAppShimApplication
// Every way of quitting the app comes through here: its menu, the Dock, and
// the quit event macOS sends at logout. The host is asked first, without
// blocking: this can be running inside a task of the message loop (a key the
// host sent back to be handled as a shortcut), where waiting for an answer in
// a nested run loop would never see it. Returning does not cancel a logout,
// which waits for the app to exit.
- (void)terminate:(id)sender {
auto* controller = electron::AppShimController::Get();
if (controller && !controller->ShouldTerminate())
return;
[super terminate:sender];
}
- (void)enableCompleteAccessibility {
EnableAccessibilitySupport(
electron::mojom::AppShimAccessibilityMode::kComplete);
}
- (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute {
// AXEnhancedUserInterface is what screen readers set; AXManualAccessibility
// is what Electron documents for other apps to ask for accessibility.
const bool is_manual = [attribute isEqualToString:@"AXManualAccessibility"];
if (is_manual || [attribute isEqualToString:@"AXEnhancedUserInterface"]) {
[NSObject
cancelPreviousPerformRequestsWithTarget:self
selector:@selector(
enableCompleteAccessibility)
object:nil];
if ([value boolValue]) {
[self performSelector:@selector(enableCompleteAccessibility)
withObject:nil
afterDelay:is_manual ? 0 : kCompleteModeDelay];
}
if (is_manual)
return;
}
[super accessibilitySetValue:value forAttribute:attribute];
}
- (NSArray*)accessibilityAttributeNames {
return [[super accessibilityAttributeNames]
arrayByAddingObject:@"AXManualAccessibility"];
}
- (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
return [attribute isEqualToString:@"AXManualAccessibility"] ||
[super accessibilityIsAttributeSettable:attribute];
}
- (NSAccessibilityRole)accessibilityRole {
// Something is reading this app's accessibility tree.
EnableAccessibilitySupport(
electron::mojom::AppShimAccessibilityMode::kPartial);
return [super accessibilityRole];
}
@end
+163
View File
@@ -0,0 +1,163 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_APP_SHIM_APP_SHIM_CONTROLLER_H_
#define ELECTRON_SHELL_APP_SHIM_APP_SHIM_CONTROLLER_H_
#import <Foundation/Foundation.h>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "electron/shell/common/app_shim.mojom.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/platform/platform_channel_endpoint.h"
#include "mojo/public/cpp/system/isolated_connection.h"
#include "shell/app_shim/app_shim_menu.h"
class GURL;
namespace display {
class ScopedNativeScreen;
} // namespace display
@class ElectronAppShimDelegate;
@class ElectronMenuController;
namespace electron {
// Runs an app shim process: finds (or starts) the host Electron app, connects
// to it, and then serves the windows the host assigns to this process.
class AppShimController : public mojom::AppShim, private AppShimMenu::Client {
public:
explicit AppShimController(const base::FilePath& host_bundle_path);
~AppShimController() override;
// The process's one controller, or null before it is made.
static AppShimController* Get();
AppShimController(const AppShimController&) = delete;
AppShimController& operator=(const AppShimController&) = delete;
// From the NSApplication delegate.
void OnReopen(bool has_visible_windows);
void OnOpenFiles(std::vector<base::FilePath> files);
void OnOpenUrls(std::vector<GURL> urls);
void OnDidBecomeActive();
void OnNewWindowForTab(uint64_t window_id);
// From -[NSApplication terminate:]. False when the host was asked instead;
// Quit(), or no answer in time, then terminates the app.
bool ShouldTerminate();
// From the NSApplication. Each mode is asked for at most once.
void EnableAccessibilitySupport(mojom::AppShimAccessibilityMode mode);
private:
// The name the host publishes its bootstrap server under.
std::string ServerName() const;
bool IsHostRunning() const;
void LaunchHost();
void PollForHost();
// Hands the host, whose bootstrap server `server_endpoint` is, one end of
// a new channel; invalid when that fails.
static mojo::PlatformChannelEndpoint ConnectToHost(
mojo::PlatformChannelEndpoint server_endpoint);
// The host is running another build of Electron than this process loaded.
// Tells the host, which tells the person, and quits.
void OnBuildMismatch(mojo::PlatformChannelEndpoint server_endpoint);
void OnConnected(mojo::PlatformChannelEndpoint endpoint);
void OnShimConnectedResponse(
mojo::PendingReceiver<mojom::AppShim> app_shim_receiver);
void Terminate();
void OnQuitAnswerTimeout();
void SetUpMenu();
// AppShimMenu::Client:
void OnMenuItemActivated(const std::vector<uint32_t>& path,
int command_id,
int event_flags) override;
void OnMenuWillShow() override;
void OnMenuWillClose() override;
// mojom::AppShim:
void CreateRemoteCocoaApplication(
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
application) override;
void SetBadgeLabel(const std::string& label) override;
void SetUserAttention(mojom::AppShimAttentionType type) override;
void Quit() override;
void CancelQuit() override;
void SetApplicationMenu(
std::optional<std::vector<mojom::AppShimMenuItemPtr>> items) override;
void InitWindowChrome(uint64_t window_id,
mojom::AppShimWindowChromePtr chrome) override;
void SetWindowButtonVisibility(uint64_t window_id, bool visible) override;
void SetWindowButtonPosition(
uint64_t window_id,
const std::optional<gfx::Point>& position) override;
void SetWindowVibrancy(uint64_t window_id,
const std::string& type,
int32_t duration_ms,
int32_t state) override;
void PerformWindowTabAction(uint64_t window_id,
mojom::AppShimTabAction action) override;
void AddTabbedWindow(uint64_t window_id, uint64_t tabbed_window_id) override;
void PreviewFile(uint64_t window_id,
const base::FilePath& path,
const std::string& display_name) override;
void CloseFilePreview(uint64_t window_id) override;
void ShowMessageBox(uint64_t window_id,
mojom::AppShimMessageBoxOptionsPtr options,
ShowMessageBoxCallback callback) override;
void CloseMessageBox(int32_t id) override;
void ShowOpenDialog(uint64_t window_id,
mojom::AppShimFileDialogOptionsPtr options,
ShowOpenDialogCallback callback) override;
void ShowSaveDialog(uint64_t window_id,
mojom::AppShimFileDialogOptionsPtr options,
ShowSaveDialogCallback callback) override;
const base::FilePath host_bundle_path_;
ElectronAppShimDelegate* __strong delegate_;
// The windows this process hosts ask it about the displays.
std::unique_ptr<display::ScopedNativeScreen> screen_;
base::TimeTicks poll_deadline_;
bool launched_host_ = false;
// The host's menu, while it has given the shim one. The controller only
// refers to the menu's model.
std::unique_ptr<AppShimMenu> menu_;
ElectronMenuController* __strong menu_controller_;
bool terminating_ = false;
// The host has been asked whether the shim may quit.
bool waiting_for_quit_answer_ = false;
base::OneShotTimer quit_answer_timer_;
bool asked_for_partial_accessibility_ = false;
bool asked_for_complete_accessibility_ = false;
NSInteger attention_request_id_ = 0;
mojo::IsolatedConnection bootstrap_connection_;
mojo::Remote<mojom::AppShimHostBootstrap> host_bootstrap_;
mojo::Receiver<mojom::AppShim> shim_receiver_{this};
mojo::Remote<mojom::AppShimHost> host_;
// Calls on `host_` made before the host binds this are delivered once it
// does.
mojo::PendingReceiver<mojom::AppShimHost> host_receiver_;
base::WeakPtrFactory<AppShimController> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_APP_SHIM_APP_SHIM_CONTROLLER_H_
+744
View File
@@ -0,0 +1,744 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/app_shim/app_shim_controller.h"
#import <Cocoa/Cocoa.h>
#include <mach/mach.h>
#include <utility>
#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/apple/scoped_mach_port.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/mac/scoped_mach_msg_destroy.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "components/remote_cocoa/app_shim/application_bridge.h"
#import "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "content/public/browser/remote_cocoa.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/platform/platform_channel.h"
#import "shell/app_shim/app_shim_window.h"
#import "shell/browser/ui/cocoa/electron_menu_controller.h"
#include "shell/browser/ui/file_dialog.h"
#include "shell/browser/ui/message_box.h"
#include "shell/common/app_shim_constants.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/display/screen.h"
#include "ui/gfx/native_ui_types.h"
#include "url/gurl.h"
namespace {
// How long a shim waits for the host app to start listening.
constexpr base::TimeDelta kPollTimeout = base::Seconds(60);
constexpr base::TimeDelta kPollPeriod = base::Milliseconds(100);
// How long the host has to answer a quit request before the shim quits anyway,
// so that a host that is busy or hung cannot hold up a logout.
constexpr base::TimeDelta kQuitAnswerTimeout = base::Seconds(5);
// How long the host has to take note of a build mismatch before the shim
// quits without knowing that it has.
constexpr base::TimeDelta kBuildMismatchAcknowledgementTimeout =
base::Seconds(2);
NSString* InfoPlistString(const char* key) {
id value = [NSBundle.mainBundle
objectForInfoDictionaryKey:base::SysUTF8ToNSString(key)];
return base::apple::ObjCCast<NSString>(value);
}
NSString* AppName() {
return base::apple::ObjCCast<NSString>([NSBundle.mainBundle
objectForInfoDictionaryKey:@"CFBundleDisplayName"])
?: base::apple::ObjCCast<NSString>([NSBundle.mainBundle
objectForInfoDictionaryKey:@"CFBundleName"])
?: NSProcessInfo.processInfo.processName;
}
} // namespace
@interface ElectronAppShimDelegate : NSObject <NSApplicationDelegate>
- (instancetype)initWithController:(electron::AppShimController*)controller;
- (void)controllerDestroyed;
@end
@implementation ElectronAppShimDelegate {
raw_ptr<electron::AppShimController> _controller;
}
- (instancetype)initWithController:(electron::AppShimController*)controller {
if ((self = [super init])) {
_controller = controller;
}
return self;
}
- (void)controllerDestroyed {
_controller = nullptr;
}
// The "+" of a window's tab bar. AppKit shows it when something in the
// responder chain answers to this, and the window's own delegate is
// remote_cocoa's.
- (void)newWindowForTab:(id)sender {
NSWindow* window = NSApp.keyWindow ?: NSApp.mainWindow;
if (!_controller || ![window isKindOfClass:[NativeWidgetMacNSWindow class]]) {
return;
}
_controller->OnNewWindowForTab(
static_cast<NativeWidgetMacNSWindow*>(window).bridgedNativeWidgetId);
}
// The Quick Look panel asks the responder chain who fills it, which ends
// here. It is the key window's, if the host asked for a preview there.
- (ElectronAppShimWindow*)windowForPreviewPanel {
ElectronAppShimWindow* window =
[ElectronAppShimWindow existingForWindow:NSApp.mainWindow];
return [window hasPreviewItem] ? window : nil;
}
- (BOOL)acceptsPreviewPanelControl:(QLPreviewPanel*)panel {
return [self windowForPreviewPanel] != nil;
}
- (void)beginPreviewPanelControl:(QLPreviewPanel*)panel {
ElectronAppShimWindow* window = [self windowForPreviewPanel];
panel.dataSource = window;
panel.delegate = window;
}
- (void)endPreviewPanelControl:(QLPreviewPanel*)panel {
panel.dataSource = nil;
panel.delegate = nil;
}
- (void)applicationDidBecomeActive:(NSNotification*)notification {
if (_controller)
_controller->OnDidBecomeActive();
}
- (BOOL)applicationShouldHandleReopen:(NSApplication*)sender
hasVisibleWindows:(BOOL)flag {
if (_controller)
_controller->OnReopen(flag);
return YES;
}
- (void)application:(NSApplication*)sender
openFiles:(NSArray<NSString*>*)filenames {
std::vector<base::FilePath> files;
for (NSString* filename in filenames)
files.push_back(base::apple::NSStringToFilePath(filename));
if (_controller)
_controller->OnOpenFiles(std::move(files));
[sender replyToOpenOrPrint:NSApplicationDelegateReplySuccess];
}
- (void)application:(NSApplication*)application
openURLs:(NSArray<NSURL*>*)urls {
std::vector<base::FilePath> files;
std::vector<GURL> other;
for (NSURL* url in urls) {
if (url.fileURL) {
files.push_back(base::apple::NSURLToFilePath(url));
} else if (GURL gurl(base::SysNSStringToUTF8(url.absoluteString));
gurl.is_valid()) {
other.push_back(std::move(gurl));
}
}
if (!_controller)
return;
if (!files.empty())
_controller->OnOpenFiles(std::move(files));
if (!other.empty())
_controller->OnOpenUrls(std::move(other));
}
- (BOOL)applicationSupportsSecureRestorableState:(NSApplication*)app {
return YES;
}
@end
namespace electron {
namespace {
AppShimController* g_controller = nullptr;
} // namespace
// static
AppShimController* AppShimController::Get() {
return g_controller;
}
AppShimController::AppShimController(const base::FilePath& host_bundle_path)
: host_bundle_path_(host_bundle_path),
host_receiver_(host_.BindNewPipeAndPassReceiver()) {
g_controller = this;
screen_ = std::make_unique<display::ScopedNativeScreen>();
delegate_ = [[ElectronAppShimDelegate alloc] initWithController:this];
NSApp.delegate = delegate_;
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
// The host decides which shims run; macOS must not bring this one back at
// login by itself.
[NSApp disableRelaunchOnLogin];
SetUpMenu();
poll_deadline_ = base::TimeTicks::Now() + kPollTimeout;
PollForHost();
}
AppShimController::~AppShimController() {
g_controller = nullptr;
[delegate_ controllerDestroyed];
NSApp.delegate = nil;
}
std::string AppShimController::ServerName() const {
// Mirrors ElectronMainDelegate::SetUpBundleOverrides in the host.
NSBundle* host = base::apple::OuterBundle();
std::string base_bundle_id = base::SysNSStringToUTF8(host.bundleIdentifier);
if (NSString* team_id = base::apple::ObjCCast<NSString>(
[host objectForInfoDictionaryKey:@"ElectronTeamID"])) {
base_bundle_id =
base::StrCat({base::SysNSStringToUTF8(team_id), ".", base_bundle_id});
}
NSString* name = InfoPlistString(app_shim::kInfoPlistServerNameKey);
return base::StrCat({base_bundle_id, ".",
name.length ? base::SysNSStringToUTF8(name)
: app_shim::kDefaultServerName});
}
bool AppShimController::IsHostRunning() const {
NSString* bundle_id = base::apple::OuterBundle().bundleIdentifier;
if (!bundle_id)
return false;
NSString* want = base::apple::FilePathToNSString(host_bundle_path_)
.stringByResolvingSymlinksInPath;
for (NSRunningApplication* app in [NSRunningApplication
runningApplicationsWithBundleIdentifier:bundle_id]) {
if (!app.terminated && [app.bundleURL.path.stringByResolvingSymlinksInPath
isEqualToString:want]) {
return true;
}
}
return false;
}
void AppShimController::LaunchHost() {
launched_host_ = true;
NSWorkspaceOpenConfiguration* configuration =
[NSWorkspaceOpenConfiguration configuration];
configuration.activates = NO;
configuration.addsToRecentItems = NO;
NSMutableArray<NSString*>* arguments = [NSMutableArray array];
for (id argument in base::apple::ObjCCast<NSArray>([NSBundle.mainBundle
objectForInfoDictionaryKey:
base::SysUTF8ToNSString(
app_shim::kInfoPlistHostArgumentsKey)])) {
if (NSString* string = base::apple::ObjCCast<NSString>(argument))
[arguments addObject:string];
}
configuration.arguments = arguments;
[NSWorkspace.sharedWorkspace
openApplicationAtURL:base::apple::FilePathToNSURL(host_bundle_path_)
configuration:configuration
completionHandler:^(NSRunningApplication* app, NSError* error) {
if (error) {
LOG(ERROR) << "Could not start the host app: "
<< base::SysNSStringToUTF8(error.localizedDescription);
}
}];
}
void AppShimController::PollForHost() {
const std::string server_name = ServerName();
if (mojo::PlatformChannelEndpoint server =
mojo::NamedPlatformChannel::ConnectToServer(server_name);
server.is_valid()) {
// The host is listening. It is only worth connecting to when it runs the
// build of Electron that this process loaded.
if (!mojo::NamedPlatformChannel::ConnectToServer(
app_shim::BuildMarkerServerName(server_name))
.is_valid()) {
OnBuildMismatch(std::move(server));
return;
}
if (mojo::PlatformChannelEndpoint endpoint =
ConnectToHost(std::move(server));
endpoint.is_valid()) {
OnConnected(std::move(endpoint));
return;
}
}
if (!launched_host_ && !IsHostRunning())
LaunchHost();
if (base::TimeTicks::Now() > poll_deadline_) {
LOG(ERROR) << "The host app did not start listening for app shims.";
Terminate();
return;
}
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&AppShimController::PollForHost,
weak_factory_.GetWeakPtr()),
kPollPeriod);
}
// static
mojo::PlatformChannelEndpoint AppShimController::ConnectToHost(
mojo::PlatformChannelEndpoint server_endpoint) {
// One bootstrap server takes connections from every shim, so each shim makes
// a channel of its own and sends the host one end of it in a raw Mach
// message. Both sides then make an isolated Mojo connection over it.
mojo::PlatformChannel channel;
mach_msg_base_t message{};
base::ScopedMachMsgDestroy scoped_message(&message.header);
message.header.msgh_id = app_shim::kBootstrapMsgId;
message.header.msgh_bits =
MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, MACH_MSG_TYPE_MOVE_SEND);
message.header.msgh_size = sizeof(message);
message.header.msgh_local_port =
channel.TakeLocalEndpoint().TakePlatformHandle().ReleaseMachSendRight();
message.header.msgh_remote_port =
server_endpoint.TakePlatformHandle().ReleaseMachSendRight();
kern_return_t kr = mach_msg_send(&message.header);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_msg_send";
return mojo::PlatformChannelEndpoint();
}
scoped_message.Disarm();
return channel.TakeRemoteEndpoint();
}
void AppShimController::OnBuildMismatch(
mojo::PlatformChannelEndpoint server_endpoint) {
LOG(ERROR) << "The host app is running another build of Electron than the "
"one on disk; it has to be restarted before this app can "
"connect to it.";
// Telling the person is the host's to do, in its own words: it gets an
// event for this message. It finds out who sent the message from the
// sender's audit token, which needs this process to still be there, so the
// host is given a moment to say that it has done so before this process
// quits. It says so by letting go of the reply right.
base::apple::ScopedMachReceiveRight reply;
if (!base::apple::CreateMachPort(&reply, nullptr)) {
Terminate();
return;
}
mach_msg_base_t message{};
base::ScopedMachMsgDestroy scoped_message(&message.header);
message.header.msgh_id = app_shim::kBuildMismatchMsgId;
message.header.msgh_bits =
MACH_MSGH_BITS(MACH_MSG_TYPE_MOVE_SEND, MACH_MSG_TYPE_MAKE_SEND_ONCE);
message.header.msgh_size = sizeof(message);
message.header.msgh_remote_port =
server_endpoint.TakePlatformHandle().ReleaseMachSendRight();
message.header.msgh_local_port = reply.get();
if (kern_return_t kr = mach_msg_send(&message.header); kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_msg_send";
Terminate();
return;
}
scoped_message.Disarm();
struct {
mach_msg_header_t header;
mach_msg_trailer_t trailer;
} acknowledgement{};
mach_msg(&acknowledgement.header, MACH_RCV_MSG | MACH_RCV_TIMEOUT, 0,
sizeof(acknowledgement), reply.get(),
kBuildMismatchAcknowledgementTimeout.InMilliseconds(),
MACH_PORT_NULL);
Terminate();
}
void AppShimController::OnConnected(mojo::PlatformChannelEndpoint endpoint) {
mojo::ScopedMessagePipeHandle message_pipe =
bootstrap_connection_.Connect(std::move(endpoint));
CHECK(message_pipe.is_valid());
host_bootstrap_.Bind(mojo::PendingRemote<mojom::AppShimHostBootstrap>(
std::move(message_pipe), 0));
host_bootstrap_.set_disconnect_handler(
base::BindOnce(&AppShimController::Terminate, base::Unretained(this)));
host_bootstrap_->OnShimConnected(
std::move(host_receiver_),
base::BindOnce(&AppShimController::OnShimConnectedResponse,
base::Unretained(this)));
}
void AppShimController::OnShimConnectedResponse(
mojo::PendingReceiver<mojom::AppShim> app_shim_receiver) {
if (!app_shim_receiver.is_valid()) {
LOG(ERROR) << "The host app turned this app shim away.";
Terminate();
return;
}
// The pipe every remote_cocoa interface is associated with. It is read on
// this runner so that a window resize can wait for the host's frames.
shim_receiver_.Bind(std::move(app_shim_receiver),
ui::WindowResizeHelperMac::Get()->task_runner());
shim_receiver_.set_disconnect_handler(
base::BindOnce(&AppShimController::Terminate, base::Unretained(this)));
host_bootstrap_.reset();
}
void AppShimController::Terminate() {
terminating_ = true;
waiting_for_quit_answer_ = false;
quit_answer_timer_.Stop();
[NSApp terminate:nil];
}
void AppShimController::OnQuitAnswerTimeout() {
LOG(WARNING) << "The host app did not answer the quit request; quitting.";
Terminate();
}
void AppShimController::OnReopen(bool has_visible_windows) {
host_->ReopenApp(has_visible_windows);
}
void AppShimController::OnOpenFiles(std::vector<base::FilePath> files) {
host_->FilesOpened(files);
}
void AppShimController::OnOpenUrls(std::vector<GURL> urls) {
host_->UrlsOpened(urls);
}
bool AppShimController::ShouldTerminate() {
if (terminating_ || !shim_receiver_.is_bound())
return true;
if (!waiting_for_quit_answer_) {
waiting_for_quit_answer_ = true;
host_->QuitRequested();
quit_answer_timer_.Start(FROM_HERE, kQuitAnswerTimeout, this,
&AppShimController::OnQuitAnswerTimeout);
}
return false;
}
void AppShimController::EnableAccessibilitySupport(
mojom::AppShimAccessibilityMode mode) {
bool& asked = mode == mojom::AppShimAccessibilityMode::kComplete
? asked_for_complete_accessibility_
: asked_for_partial_accessibility_;
if (asked || asked_for_complete_accessibility_)
return;
asked = true;
host_->EnableAccessibilitySupport(mode);
}
void AppShimController::CreateRemoteCocoaApplication(
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
application) {
remote_cocoa::ApplicationBridge::Get()->BindReceiver(std::move(application));
remote_cocoa::ApplicationBridge::Get()->SetContentNSViewCreateCallbacks(
base::BindRepeating(
[](uint64_t view_id, mojo::ScopedInterfaceEndpointHandle host_handle,
mojo::ScopedInterfaceEndpointHandle view_request_handle) {
remote_cocoa::CreateRenderWidgetHostNSView(
view_id, std::move(host_handle), std::move(view_request_handle),
remote_cocoa::RenderWidgetHostViewMacDelegateCallback());
}),
base::BindRepeating(remote_cocoa::CreateWebContentsNSView));
}
void AppShimController::SetBadgeLabel(const std::string& label) {
NSApp.dockTile.badgeLabel =
label.empty() ? nil : base::SysUTF8ToNSString(label);
}
void AppShimController::SetUserAttention(mojom::AppShimAttentionType type) {
switch (type) {
case mojom::AppShimAttentionType::kCancel:
[NSApp cancelUserAttentionRequest:attention_request_id_];
break;
case mojom::AppShimAttentionType::kInformational:
attention_request_id_ =
[NSApp requestUserAttention:NSInformationalRequest];
break;
case mojom::AppShimAttentionType::kCritical:
attention_request_id_ = [NSApp requestUserAttention:NSCriticalRequest];
break;
}
}
void AppShimController::Quit() {
Terminate();
}
namespace {
// The NSWindow that the host knows as `window_id`, if it is still there.
gfx::NativeWindow WindowForId(uint64_t window_id) {
auto* bridge = remote_cocoa::NativeWidgetNSWindowBridge::GetFromId(window_id);
return gfx::NativeWindow(bridge ? bridge->ns_window() : nil);
}
file_dialog::DialogSettings FromMojo(
const mojom::AppShimFileDialogOptions& options) {
file_dialog::DialogSettings settings;
settings.title = options.title;
settings.message = options.message;
settings.button_label = options.button_label;
settings.name_field_label = options.name_field_label;
settings.default_path = options.default_path;
for (const auto& filter : options.filters)
settings.filters.emplace_back(filter->name, filter->extensions);
settings.properties = options.properties;
settings.shows_tag_field = options.shows_tag_field;
return settings;
}
} // namespace
void AppShimController::InitWindowChrome(uint64_t window_id,
mojom::AppShimWindowChromePtr chrome) {
[[ElectronAppShimWindow forWindowId:window_id] initChrome:*chrome];
}
void AppShimController::SetWindowButtonVisibility(uint64_t window_id,
bool visible) {
[[ElectronAppShimWindow forWindowId:window_id] setButtonVisibility:visible];
}
void AppShimController::SetWindowButtonPosition(
uint64_t window_id,
const std::optional<gfx::Point>& position) {
[[ElectronAppShimWindow forWindowId:window_id] setButtonPosition:position];
}
void AppShimController::SetWindowVibrancy(uint64_t window_id,
const std::string& type,
int32_t duration_ms,
int32_t state) {
[[ElectronAppShimWindow forWindowId:window_id]
setVibrancy:type
duration:duration_ms
state:static_cast<NSVisualEffectState>(state)];
}
void AppShimController::PerformWindowTabAction(uint64_t window_id,
mojom::AppShimTabAction action) {
[[ElectronAppShimWindow forWindowId:window_id] performTabAction:action];
}
void AppShimController::AddTabbedWindow(uint64_t window_id,
uint64_t tabbed_window_id) {
[[ElectronAppShimWindow forWindowId:window_id]
addTabbedWindow:[ElectronAppShimWindow forWindowId:tabbed_window_id]];
}
void AppShimController::PreviewFile(uint64_t window_id,
const base::FilePath& path,
const std::string& display_name) {
[[ElectronAppShimWindow forWindowId:window_id]
previewFile:base::apple::FilePathToNSURL(path)
title:base::SysUTF8ToNSString(display_name)];
}
void AppShimController::CloseFilePreview(uint64_t window_id) {
[[ElectronAppShimWindow forWindowId:window_id] closeFilePreview];
}
void AppShimController::OnNewWindowForTab(uint64_t window_id) {
if (shim_receiver_.is_bound())
host_->NewWindowForTab(window_id);
}
void AppShimController::ShowMessageBox(
uint64_t window_id,
mojom::AppShimMessageBoxOptionsPtr options,
ShowMessageBoxCallback callback) {
const gfx::NativeWindow window = WindowForId(window_id);
if (!window) {
std::move(callback).Run(options->cancel_id, false);
return;
}
MessageBoxSettings settings;
settings.type = static_cast<MessageBoxType>(options->type);
settings.buttons = options->buttons;
settings.default_id = options->default_id;
settings.cancel_id = options->cancel_id;
settings.message = options->message;
settings.detail = options->detail;
settings.checkbox_label = options->checkbox_label;
settings.checkbox_checked = options->checkbox_checked;
settings.icon = options->icon;
settings.text_width = options->text_width;
if (options->has_id)
settings.id = options->id;
ShowMessageBoxForNSWindow(settings, window, std::move(callback));
}
void AppShimController::CloseMessageBox(int32_t id) {
electron::CloseMessageBox(id);
}
void AppShimController::ShowOpenDialog(
uint64_t window_id,
mojom::AppShimFileDialogOptionsPtr options,
ShowOpenDialogCallback callback) {
const gfx::NativeWindow window = WindowForId(window_id);
if (!window) {
std::move(callback).Run(true, {});
return;
}
file_dialog::ShowOpenDialogForNSWindow(FromMojo(*options), window,
std::move(callback));
}
void AppShimController::ShowSaveDialog(
uint64_t window_id,
mojom::AppShimFileDialogOptionsPtr options,
ShowSaveDialogCallback callback) {
const gfx::NativeWindow window = WindowForId(window_id);
if (!window) {
std::move(callback).Run(true, base::FilePath());
return;
}
file_dialog::ShowSaveDialogForNSWindow(FromMojo(*options), window,
std::move(callback));
}
void AppShimController::SetApplicationMenu(
std::optional<std::vector<mojom::AppShimMenuItemPtr>> items) {
if (!items) {
if (menu_) {
menu_controller_ = nil;
menu_.reset();
SetUpMenu();
}
return;
}
// The usual case is the same menu with another state, which the menu on
// screen takes from the model the next time it looks.
if (menu_ && menu_->Update(&*items))
return;
// The controller refers to the model, so it goes first.
menu_controller_ = nil;
menu_ =
std::make_unique<AppShimMenu>(std::move(*items), std::vector<uint32_t>(),
static_cast<AppShimMenu::Client*>(this));
menu_controller_ =
[[ElectronMenuController alloc] initWithModel:menu_->model()
useDefaultAccelerator:YES];
[NSApp setMainMenu:[menu_controller_ menu]];
}
void AppShimController::OnMenuItemActivated(const std::vector<uint32_t>& path,
int command_id,
int event_flags) {
if (shim_receiver_.is_bound())
host_->MenuItemActivated(path, command_id, event_flags);
}
void AppShimController::OnMenuWillShow() {
if (shim_receiver_.is_bound())
host_->MenuWillShow();
}
void AppShimController::OnMenuWillClose() {
if (shim_receiver_.is_bound())
host_->MenuWillClose();
}
void AppShimController::OnDidBecomeActive() {
if (shim_receiver_.is_bound())
host_->DidBecomeActive();
}
void AppShimController::CancelQuit() {
waiting_for_quit_answer_ = false;
quit_answer_timer_.Stop();
}
void AppShimController::SetUpMenu() {
NSString* name = AppName();
NSMenu* main_menu = [[NSMenu alloc] initWithTitle:@""];
NSMenuItem* app_item = [main_menu addItemWithTitle:name
action:nil
keyEquivalent:@""];
NSMenu* app_menu = [[NSMenu alloc] initWithTitle:name];
[app_menu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", name]
action:@selector(hide:)
keyEquivalent:@"h"];
NSMenuItem* hide_others =
[app_menu addItemWithTitle:@"Hide Others"
action:@selector(hideOtherApplications:)
keyEquivalent:@"h"];
hide_others.keyEquivalentModifierMask =
NSEventModifierFlagCommand | NSEventModifierFlagOption;
[app_menu addItemWithTitle:@"Show All"
action:@selector(unhideAllApplications:)
keyEquivalent:@""];
[app_menu addItem:[NSMenuItem separatorItem]];
[app_menu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", name]
action:@selector(terminate:)
keyEquivalent:@"q"];
app_item.submenu = app_menu;
NSMenuItem* edit_item = [main_menu addItemWithTitle:@"Edit"
action:nil
keyEquivalent:@""];
NSMenu* edit_menu = [[NSMenu alloc] initWithTitle:@"Edit"];
[edit_menu addItemWithTitle:@"Undo"
action:@selector(undo:)
keyEquivalent:@"z"];
[edit_menu addItemWithTitle:@"Redo"
action:@selector(redo:)
keyEquivalent:@"Z"];
[edit_menu addItem:[NSMenuItem separatorItem]];
[edit_menu addItemWithTitle:@"Cut" action:@selector(cut:) keyEquivalent:@"x"];
[edit_menu addItemWithTitle:@"Copy"
action:@selector(copy:)
keyEquivalent:@"c"];
[edit_menu addItemWithTitle:@"Paste"
action:@selector(paste:)
keyEquivalent:@"v"];
[edit_menu addItemWithTitle:@"Select All"
action:@selector(selectAll:)
keyEquivalent:@"a"];
edit_item.submenu = edit_menu;
NSMenuItem* window_item = [main_menu addItemWithTitle:@"Window"
action:nil
keyEquivalent:@""];
NSMenu* window_menu = [[NSMenu alloc] initWithTitle:@"Window"];
[window_menu addItemWithTitle:@"Minimize"
action:@selector(performMiniaturize:)
keyEquivalent:@"m"];
[window_menu addItemWithTitle:@"Zoom"
action:@selector(performZoom:)
keyEquivalent:@""];
[window_menu addItemWithTitle:@"Close"
action:@selector(performClose:)
keyEquivalent:@"w"];
[window_menu addItem:[NSMenuItem separatorItem]];
[window_menu addItemWithTitle:@"Bring All to Front"
action:@selector(arrangeInFront:)
keyEquivalent:@""];
window_item.submenu = window_menu;
NSApp.windowsMenu = window_menu;
NSApp.mainMenu = main_menu;
}
} // namespace electron
+83
View File
@@ -0,0 +1,83 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
// The executable of an app shim: a small app, in a bundle of its own, whose
// windows are drawn by a host Electron app. It links nothing from Electron, so
// one build of it works with any later host: it loads the host's framework and
// calls ElectronAppShimMain.
//
// The shim bundle's Info.plist names the host with one of:
// ElectronAppShimHostPath the host .app's absolute path
// ElectronAppShimHostBundleIdentifier the host's bundle identifier
// See docs/api/app-shim-host.md.
#import <Cocoa/Cocoa.h>
#include <dlfcn.h>
#include <stdlib.h>
namespace {
typedef int (*AppShimMain)(int argc, char* argv[]);
NSString* StringForKey(NSString* key) {
id value = [NSBundle.mainBundle objectForInfoDictionaryKey:key];
return [value isKindOfClass:[NSString class]] && [value length] ? value : nil;
}
[[noreturn]] void Fail(NSString* message) {
NSLog(@"app shim: %@", message);
NSAlert* alert = [[NSAlert alloc] init];
alert.messageText = @"This app can’t be opened.";
alert.informativeText = message;
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
[alert runModal];
exit(1);
}
NSString* HostBundlePath() {
// The path wins while the host is still there; a host that has been moved
// is found again by its identifier.
NSString* path = StringForKey(@"ElectronAppShimHostPath");
if (path && [NSFileManager.defaultManager fileExistsAtPath:path])
return path;
if (NSString* bundle_id =
StringForKey(@"ElectronAppShimHostBundleIdentifier")) {
if (NSURL* url = [NSWorkspace.sharedWorkspace
URLForApplicationWithBundleIdentifier:bundle_id]) {
return url.path;
}
}
return path;
}
} // namespace
int main(int argc, char* argv[]) {
AppShimMain app_shim_main = nullptr;
@autoreleasepool {
NSString* host = HostBundlePath();
if (!host)
Fail(@"The app it belongs to could not be found.");
NSString* framework_name =
StringForKey(@"ElectronAppShimFrameworkName") ?: @"Electron Framework";
NSString* framework =
[NSString stringWithFormat:@"%@/Contents/Frameworks/%@.framework/%@",
host, framework_name, framework_name];
void* library = dlopen(framework.fileSystemRepresentation, RTLD_NOW);
if (!library) {
Fail([NSString stringWithFormat:@"%@ could not be loaded: %s",
framework_name, dlerror()]);
}
app_shim_main =
reinterpret_cast<AppShimMain>(dlsym(library, "ElectronAppShimMain"));
if (!app_shim_main) {
Fail([NSString stringWithFormat:@"%@ is too old to run this app.",
host.lastPathComponent]);
}
}
return app_shim_main(argc, argv);
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_APP_SHIM_APP_SHIM_MAIN_DELEGATE_H_
#define ELECTRON_SHELL_APP_SHIM_APP_SHIM_MAIN_DELEGATE_H_
#include <memory>
#include <string>
#include <variant>
#include "base/files/file_path.h"
#include "content/public/app/content_main_delegate.h"
namespace electron {
class AppShimController;
// The content delegate of an app shim process: a process that loads the
// framework of a host Electron app, connects to it, and hosts the NSWindows of
// the browser windows that the host app assigns to it.
class AppShimMainDelegate : public content::ContentMainDelegate {
public:
// `host_bundle_path` is the .app bundle of the host Electron app.
explicit AppShimMainDelegate(const base::FilePath& host_bundle_path);
~AppShimMainDelegate() override;
AppShimMainDelegate(const AppShimMainDelegate&) = delete;
AppShimMainDelegate& operator=(const AppShimMainDelegate&) = delete;
private:
// content::ContentMainDelegate:
std::optional<int> BasicStartupComplete() override;
void PreSandboxStartup() override;
std::variant<int, content::MainFunctionParams> RunProcess(
const std::string& process_type,
content::MainFunctionParams main_function_params) override;
bool ShouldCreateFeatureList(InvokedIn invoked_in) override;
bool ShouldInitializeMojo(InvokedIn invoked_in) override;
bool ShouldInitializePerfetto(InvokedIn invoked_in) override;
bool ShouldReconfigurePartitionAlloc() override;
bool ShouldLoadV8Snapshot(const std::string& process_type) override;
content::ContentClient* CreateContentClient() override;
const base::FilePath host_bundle_path_;
std::unique_ptr<content::ContentClient> content_client_;
std::unique_ptr<AppShimController> controller_;
};
} // namespace electron
#endif // ELECTRON_SHELL_APP_SHIM_APP_SHIM_MAIN_DELEGATE_H_
+148
View File
@@ -0,0 +1,148 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/app_shim/app_shim_main_delegate.h"
#import <Cocoa/Cocoa.h>
#include <utility>
#include "base/base_switches.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/logging/logging_settings.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_executor.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/threading/platform_thread.h"
#include "base/threading/thread.h"
#include "components/remote_cocoa/app_shim/application_bridge.h"
#include "mojo/core/embedder/configuration.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/core/embedder/scoped_ipc_support.h"
#include "mojo/public/cpp/system/functions.h"
#include "shell/app/electron_content_client.h"
#include "shell/app/electron_main_delegate.h"
#import "shell/app_shim/app_shim_application.h"
#include "shell/app_shim/app_shim_controller.h"
#include "shell/common/app_shim_constants.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/l10n/l10n_util_mac.h"
namespace electron {
namespace {
void HandleBadMessage(const std::string& error) {
LOG(ERROR) << "Mojo error in app shim process: " << error;
}
} // namespace
AppShimMainDelegate::AppShimMainDelegate(const base::FilePath& host_bundle_path)
: host_bundle_path_(host_bundle_path) {}
AppShimMainDelegate::~AppShimMainDelegate() = default;
std::optional<int> AppShimMainDelegate::BasicStartupComplete() {
logging::LoggingSettings settings;
settings.logging_dest =
logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR;
logging::InitLogging(settings);
remote_cocoa::ApplicationBridge::SetIsOutOfProcessAppShim();
return std::nullopt;
}
void AppShimMainDelegate::PreSandboxStartup() {
// A shim is started by Launch Services, not by the browser, so it has no
// field trial state to inherit: features are the defaults, changed only by
// this process's own command line.
const base::CommandLine* command_line =
base::CommandLine::ForCurrentProcess();
base::FeatureList::InitInstance(
command_line->GetSwitchValueASCII(::switches::kEnableFeatures),
command_line->GetSwitchValueASCII(::switches::kDisableFeatures));
// Localized strings and cursors for the views that draw in this process, in
// the language the host app would pick.
l10n_util::OverrideLocaleWithCocoaLocale();
LoadResourceBundle(l10n_util::GetLocaleOverride());
// Mojo's features must match the browser's to speak the same IPC.
mojo::core::InitFeatures();
// The connection to the browser is isolated, so this process is a broker.
mojo::core::Configuration config;
config.is_broker_process = true;
mojo::core::Init(config);
mojo::SetDefaultProcessErrorHandler(base::BindRepeating(&HandleBadMessage));
}
std::variant<int, content::MainFunctionParams> AppShimMainDelegate::RunProcess(
const std::string& process_type,
content::MainFunctionParams main_function_params) {
if (process_type != app_shim::kProcessType)
return std::move(main_function_params);
base::Thread::Options io_thread_options;
io_thread_options.message_pump_type = base::MessagePumpType::IO;
// Leaked on purpose: it must outlive every Mojo object in the process.
base::Thread* io_thread = new base::Thread("ElectronAppShimIO");
io_thread->StartWithOptions(std::move(io_thread_options));
mojo::core::ScopedIPCSupport ipc_support(
io_thread->task_runner(),
mojo::core::ScopedIPCSupport::ShutdownPolicy::FAST);
// The NSApplication must be the shim's own, so must not exist yet.
[ElectronAppShimApplication sharedApplication];
CHECK([NSApp isKindOfClass:[ElectronAppShimApplication class]]);
base::SingleThreadTaskExecutor main_task_executor(base::MessagePumpType::UI,
/*is_main_thread=*/true);
ui::WindowResizeHelperMac::Get()->Init(main_task_executor.task_runner());
// content made the pool for this process type but only starts it for the
// process types it runs itself.
base::ThreadPoolInstance::Get()->StartWithDefaultParams();
base::PlatformThread::SetName("ElectronAppShimMain");
controller_ = std::make_unique<AppShimController>(host_bundle_path_);
base::RunLoop().Run();
return 0;
}
bool AppShimMainDelegate::ShouldCreateFeatureList(InvokedIn invoked_in) {
// Created in PreSandboxStartup.
return false;
}
bool AppShimMainDelegate::ShouldInitializeMojo(InvokedIn invoked_in) {
// Initialized in PreSandboxStartup, as a broker.
return false;
}
bool AppShimMainDelegate::ShouldInitializePerfetto(InvokedIn invoked_in) {
return false;
}
bool AppShimMainDelegate::ShouldReconfigurePartitionAlloc() {
return false;
}
bool AppShimMainDelegate::ShouldLoadV8Snapshot(
const std::string& process_type) {
// An app shim runs no JavaScript.
return false;
}
content::ContentClient* AppShimMainDelegate::CreateContentClient() {
content_client_ = std::make_unique<ElectronContentClient>();
return content_client_.get();
}
} // namespace electron
+73
View File
@@ -0,0 +1,73 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_APP_SHIM_APP_SHIM_MENU_H_
#define ELECTRON_SHELL_APP_SHIM_APP_SHIM_MENU_H_
#include <memory>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/scoped_observation.h"
#include "electron/shell/common/app_shim.mojom.h"
#include "shell/browser/ui/electron_menu_model.h"
namespace electron {
// A menu, and the menus under it, that the host app described. It gives
// ElectronMenuController a model to build an NSMenu from, as it would have in
// the host's process, and passes on what the person does with it.
class AppShimMenu : public ElectronMenuModel::Delegate,
private ElectronMenuModel::Observer {
public:
class Client {
public:
virtual void OnMenuItemActivated(const std::vector<uint32_t>& path,
int command_id,
int event_flags) = 0;
virtual void OnMenuWillShow() = 0;
virtual void OnMenuWillClose() = 0;
protected:
virtual ~Client() = default;
};
// `client` must outlive this. `path` is where this menu is under the menu
// bar: empty for the menu bar itself.
AppShimMenu(std::vector<mojom::AppShimMenuItemPtr> items,
std::vector<uint32_t> path,
Client* client);
~AppShimMenu() override;
AppShimMenu(const AppShimMenu&) = delete;
AppShimMenu& operator=(const AppShimMenu&) = delete;
ElectronMenuModel* model() { return model_.get(); }
// Takes the state of `items` (labels, checks and so on) if they are the
// same menu as this one, item for item. False, changing nothing, when they
// are not, and an NSMenu built from this would be the wrong one.
bool Update(std::vector<mojom::AppShimMenuItemPtr>* items);
private:
class Item;
bool HasShapeOf(const std::vector<mojom::AppShimMenuItemPtr>& items) const;
// ElectronMenuModel::Delegate:
void ActivatedAt(size_t index, int event_flags) override;
void MenuWillShow() override;
// ElectronMenuModel::Observer:
void OnMenuWillClose() override;
const std::vector<uint32_t> path_;
const raw_ptr<Client> client_;
std::vector<std::unique_ptr<Item>> items_;
std::unique_ptr<ElectronMenuModel> model_;
};
} // namespace electron
#endif // ELECTRON_SHELL_APP_SHIM_APP_SHIM_MENU_H_
+160
View File
@@ -0,0 +1,160 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/app_shim/app_shim_menu.h"
#include <utility>
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/models/image_model.h"
namespace electron {
class AppShimMenu::Item : public ElectronMenuModel::Item {
public:
Item(mojom::AppShimMenuItemPtr data,
std::vector<uint32_t> path,
AppShimMenu::Client* client)
: data_(std::move(data)) {
if (data_->submenu) {
submenu_ = std::make_unique<AppShimMenu>(std::move(*data_->submenu),
std::move(path), client);
data_->submenu.reset();
}
}
const mojom::AppShimMenuItem& data() const { return *data_; }
AppShimMenu* submenu() const { return submenu_.get(); }
// What a built NSMenuItem can't be given afterwards.
bool HasShapeOf(const mojom::AppShimMenuItem& other) const {
return data_->type == other.type && data_->command_id == other.command_id &&
data_->role == other.role && data_->tool_tip == other.tool_tip &&
data_->custom_type == other.custom_type &&
data_->has_accelerator == other.has_accelerator &&
data_->accelerator_key_code == other.accelerator_key_code &&
data_->accelerator_modifiers == other.accelerator_modifiers &&
data_->accelerator_shifted_char == other.accelerator_shifted_char &&
data_->register_accelerator == other.register_accelerator &&
data_->works_when_hidden == other.works_when_hidden &&
data_->group_id == other.group_id &&
!!submenu_ == other.submenu.has_value() &&
(!submenu_ || submenu_->HasShapeOf(*other.submenu));
}
void Update(mojom::AppShimMenuItemPtr data) {
if (submenu_)
submenu_->Update(&*data->submenu);
data_ = std::move(data);
data_->submenu.reset();
}
// ElectronMenuModel::Item:
ui::MenuModel::ItemType GetType() const override {
return static_cast<ui::MenuModel::ItemType>(data_->type);
}
int GetCommandId() const override { return data_->command_id; }
std::u16string GetLabel() const override { return data_->label; }
std::u16string GetSecondaryLabel() const override {
return data_->secondary_label;
}
std::u16string GetToolTip() const override { return data_->tool_tip; }
std::u16string GetAccessibilityLabel() const override {
return data_->accessibility_label;
}
std::u16string GetRole() const override { return data_->role; }
std::u16string GetCustomType() const override { return data_->custom_type; }
ui::ImageModel GetIcon() const override {
return data_->icon.isNull() ? ui::ImageModel()
: ui::ImageModel::FromImageSkia(data_->icon);
}
bool GetAccelerator(ui::Accelerator* accelerator) const override {
if (!data_->has_accelerator)
return false;
*accelerator = ui::Accelerator(
static_cast<ui::KeyboardCode>(data_->accelerator_key_code),
data_->accelerator_modifiers);
if (data_->accelerator_shifted_char)
accelerator->shifted_char = data_->accelerator_shifted_char;
return true;
}
bool ShouldRegisterAccelerator() const override {
return data_->register_accelerator;
}
bool WorksWhenHidden() const override { return data_->works_when_hidden; }
bool IsChecked() const override { return data_->checked; }
bool IsEnabled() const override { return data_->enabled; }
bool IsVisible() const override { return data_->visible; }
int GetGroupId() const override { return data_->group_id; }
ElectronMenuModel* GetSubmenuModel() const override {
return submenu_ ? submenu_->model() : nullptr;
}
std::optional<ElectronMenuModel::SharingItem> GetSharingItem() override {
return std::nullopt;
}
const ElectronMenuModel::Badge* GetBadge() const override { return nullptr; }
private:
mojom::AppShimMenuItemPtr data_;
std::unique_ptr<AppShimMenu> submenu_;
};
AppShimMenu::AppShimMenu(std::vector<mojom::AppShimMenuItemPtr> items,
std::vector<uint32_t> path,
Client* client)
: path_(std::move(path)),
client_(client),
model_(std::make_unique<ElectronMenuModel>(this)) {
for (auto& item : items) {
std::vector<uint32_t> item_path = path_;
item_path.push_back(items_.size());
items_.push_back(
std::make_unique<Item>(std::move(item), std::move(item_path), client));
model_->InsertItemAt(items_.size() - 1, items_.back().get());
}
// Only the menu bar's model is told that a menu opens and closes.
if (path_.empty())
model_->AddObserver(this);
}
AppShimMenu::~AppShimMenu() {
if (path_.empty())
model_->RemoveObserver(this);
}
bool AppShimMenu::HasShapeOf(
const std::vector<mojom::AppShimMenuItemPtr>& items) const {
if (items.size() != items_.size())
return false;
for (size_t i = 0; i < items.size(); ++i) {
if (!items_[i]->HasShapeOf(*items[i]))
return false;
}
return true;
}
bool AppShimMenu::Update(std::vector<mojom::AppShimMenuItemPtr>* items) {
if (!HasShapeOf(*items))
return false;
for (size_t i = 0; i < items->size(); ++i)
items_[i]->Update(std::move((*items)[i]));
return true;
}
void AppShimMenu::ActivatedAt(size_t index, int event_flags) {
std::vector<uint32_t> item_path = path_;
item_path.push_back(index);
client_->OnMenuItemActivated(item_path, items_[index]->data().command_id,
event_flags);
}
void AppShimMenu::MenuWillShow() {
client_->OnMenuWillShow();
}
void AppShimMenu::OnMenuWillClose() {
client_->OnMenuWillClose();
}
} // namespace electron
+46
View File
@@ -0,0 +1,46 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_APP_SHIM_APP_SHIM_WINDOW_H_
#define ELECTRON_SHELL_APP_SHIM_APP_SHIM_WINDOW_H_
#import <Cocoa/Cocoa.h>
#import <Quartz/Quartz.h>
#include <optional>
#include <string>
#include "electron/shell/common/app_shim.mojom.h"
#include "ui/gfx/geometry/point.h"
// The parts of a window in an app shim that the host app asks for and that
// remote_cocoa has no call for: the window buttons of a hidden title bar,
// vibrancy and the Quick Look preview. What NativeWindowMac does to its own
// NSWindow for a window of the host's process, this does to the NSWindow that
// remote_cocoa made here.
@interface ElectronAppShimWindow
: NSObject <QLPreviewPanelDataSource, QLPreviewPanelDelegate>
// The object for the window that the host knows by `windowId`, made on first
// use. Nil when there is no such window (any more).
+ (instancetype)forWindowId:(uint64_t)windowId;
// The object for `window`, if one has been made.
+ (instancetype)existingForWindow:(NSWindow*)window;
- (void)initChrome:(const electron::mojom::AppShimWindowChrome&)chrome;
- (void)setButtonVisibility:(BOOL)visible;
- (void)setButtonPosition:(const std::optional<gfx::Point>&)position;
- (void)setVibrancy:(const std::string&)type
duration:(int)durationMs
state:(NSVisualEffectState)state;
- (void)performTabAction:(electron::mojom::AppShimTabAction)action;
- (void)addTabbedWindow:(ElectronAppShimWindow*)other;
- (void)previewFile:(NSURL*)url title:(NSString*)title;
- (void)closeFilePreview;
// Whether the Quick Look panel is this window's to fill.
- (BOOL)hasPreviewItem;
@end
#endif // ELECTRON_SHELL_APP_SHIM_APP_SHIM_WINDOW_H_
+332
View File
@@ -0,0 +1,332 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#import "shell/app_shim/app_shim_window.h"
#include <objc/runtime.h>
#include "base/strings/sys_string_conversions.h"
#import "components/remote_cocoa/app_shim/native_widget_mac_nswindow.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "shell/browser/native_window.h"
#import "shell/browser/ui/cocoa/electron_preview_item.h"
#import "shell/browser/ui/cocoa/window_buttons_proxy.h"
namespace {
using TitleBarStyle = electron::NativeWindow::TitleBarStyle;
const char kAssociationKey = 0;
// 0 for a name that is not a vibrancy type.
NSVisualEffectMaterial MaterialForVibrancyType(const std::string& type) {
static const struct {
const char* type;
NSVisualEffectMaterial material;
} kMaterials[] = {
{"titlebar", NSVisualEffectMaterialTitlebar},
{"selection", NSVisualEffectMaterialSelection},
{"menu", NSVisualEffectMaterialMenu},
{"popover", NSVisualEffectMaterialPopover},
{"sidebar", NSVisualEffectMaterialSidebar},
{"header", NSVisualEffectMaterialHeaderView},
{"sheet", NSVisualEffectMaterialSheet},
{"window", NSVisualEffectMaterialWindowBackground},
{"hud", NSVisualEffectMaterialHUDWindow},
{"fullscreen-ui", NSVisualEffectMaterialFullScreenUI},
{"tooltip", NSVisualEffectMaterialToolTip},
{"content", NSVisualEffectMaterialContentBackground},
{"under-window", NSVisualEffectMaterialUnderWindowBackground},
{"under-page", NSVisualEffectMaterialUnderPageBackground},
};
for (const auto& entry : kMaterials) {
if (type == entry.type)
return entry.material;
}
return static_cast<NSVisualEffectMaterial>(0);
}
} // namespace
@implementation ElectronAppShimWindow {
NSWindow* __weak _window;
BOOL _hasFrame;
TitleBarStyle _titleBarStyle;
WindowButtonsProxy* __strong _buttonsProxy;
BOOL _buttonsVisible;
NSVisualEffectView* __strong _vibrantView;
ElectronPreviewItem* __strong _previewItem;
}
+ (instancetype)forWindowId:(uint64_t)windowId {
auto* bridge = remote_cocoa::NativeWidgetNSWindowBridge::GetFromId(windowId);
NSWindow* window = bridge ? bridge->ns_window() : nil;
if (!window)
return nil;
ElectronAppShimWindow* existing = [self existingForWindow:window];
if (existing)
return existing;
ElectronAppShimWindow* made = [[self alloc] initWithWindow:window];
objc_setAssociatedObject(window, &kAssociationKey, made,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return made;
}
+ (instancetype)existingForWindow:(NSWindow*)window {
return window ? objc_getAssociatedObject(window, &kAssociationKey) : nil;
}
- (instancetype)initWithWindow:(NSWindow*)window {
if ((self = [super init])) {
_window = window;
_hasFrame = YES;
_titleBarStyle = TitleBarStyle::kNormal;
_buttonsVisible = YES;
NSNotificationCenter* center = NSNotificationCenter.defaultCenter;
[center addObserver:self
selector:@selector(windowDidResize:)
name:NSWindowDidResizeNotification
object:window];
for (NSNotificationName name in @[
NSWindowDidBecomeMainNotification, NSWindowDidResignMainNotification,
NSWindowDidBecomeKeyNotification, NSWindowDidResignKeyNotification,
NSWindowDidChangeOcclusionStateNotification
]) {
[center addObserver:self
selector:@selector(windowDidResize:)
name:name
object:window];
}
[center addObserver:self
selector:@selector(windowDidEnterFullScreen:)
name:NSWindowDidEnterFullScreenNotification
object:window];
[center addObserver:self
selector:@selector(windowWillExitFullScreen:)
name:NSWindowWillExitFullScreenNotification
object:window];
[center addObserver:self
selector:@selector(windowDidExitFullScreen:)
name:NSWindowDidExitFullScreenNotification
object:window];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
- (void)setStandardButtonsHidden:(BOOL)hidden {
[_window standardWindowButton:NSWindowCloseButton].hidden = hidden;
[_window standardWindowButton:NSWindowMiniaturizeButton].hidden = hidden;
[_window standardWindowButton:NSWindowZoomButton].hidden = hidden;
}
- (void)initChrome:(const electron::mojom::AppShimWindowChrome&)chrome {
_hasFrame = chrome.has_frame;
_titleBarStyle = static_cast<TitleBarStyle>(chrome.title_bar_style);
if (chrome.tabbing_identifier && _hasFrame) {
_window.tabbingIdentifier =
base::SysUTF8ToNSString(*chrome.tabbing_identifier);
} else {
_window.tabbingMode = NSWindowTabbingModeDisallowed;
}
if (_hasFrame)
return;
// The window has a title bar that is see-through and has no title. Whether
// its buttons show is the title bar style's to say.
if (_titleBarStyle == TitleBarStyle::kNormal) {
[self setStandardButtonsHidden:YES];
return;
}
_buttonsProxy = [[WindowButtonsProxy alloc] initWithWindow:_window];
[_buttonsProxy setHeight:chrome.titlebar_overlay_height];
if (chrome.traffic_light_position) {
[_buttonsProxy setMargin:*chrome.traffic_light_position];
} else if (_titleBarStyle == TitleBarStyle::kHiddenInset) {
[_buttonsProxy setMargin:gfx::Point(12, 11)];
}
if (_titleBarStyle == TitleBarStyle::kCustomButtonsOnHover) {
[_buttonsProxy setShowOnHover:YES];
} else {
[self setStandardButtonsHidden:NO];
}
}
- (void)setButtonVisibility:(BOOL)visible {
_buttonsVisible = visible;
if (_buttonsProxy) {
if (visible)
[_buttonsProxy redraw];
[_buttonsProxy setVisible:visible];
}
if (_titleBarStyle != TitleBarStyle::kCustomButtonsOnHover)
[self setStandardButtonsHidden:!visible];
}
- (void)setButtonPosition:(const std::optional<gfx::Point>&)position {
[_buttonsProxy setMargin:position];
}
// Also when the window comes on screen, and when it becomes or stops being
// the main or key window: each lays the title bar out again, which puts the
// buttons back where the system has them. The layout may come after the
// notification, so the buttons are placed once more behind it.
- (void)windowDidResize:(NSNotification*)notification {
[self redrawButtons];
ElectronAppShimWindow* __weak weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf redrawButtons];
});
}
- (void)redrawButtons {
if (!(_window.styleMask & NSWindowStyleMaskFullScreen))
[_buttonsProxy redraw];
}
- (void)windowDidEnterFullScreen:(NSNotification*)notification {
// The title comes back for full screen, where the title bar slides in.
if (_buttonsProxy)
_window.titleVisibility = NSWindowTitleVisible;
if (!_hasFrame)
_window.titlebarAppearsTransparent = NO;
}
- (void)windowWillExitFullScreen:(NSNotification*)notification {
if (_buttonsProxy) {
_window.titleVisibility = NSWindowTitleHidden;
// Hidden meanwhile, or the buttons jump.
[_buttonsProxy setVisible:NO];
}
}
- (void)windowDidExitFullScreen:(NSNotification*)notification {
if (_buttonsProxy && _buttonsVisible) {
[_buttonsProxy redraw];
[_buttonsProxy setVisible:YES];
}
if (!_hasFrame)
_window.titlebarAppearsTransparent = YES;
}
- (void)setVibrancy:(const std::string&)type
duration:(int)durationMs
state:(NSVisualEffectState)state {
const BOOL animate = durationMs > 0;
NSVisualEffectView* vibrantView = _vibrantView;
if (type.empty()) {
if (!vibrantView)
return;
_vibrantView = nil;
if (animate) {
[NSAnimationContext
runAnimationGroup:^(NSAnimationContext* context) {
context.duration = durationMs / 1000.0;
vibrantView.animator.alphaValue = 0.0;
}
completionHandler:^{
[vibrantView removeFromSuperview];
}];
} else {
[vibrantView removeFromSuperview];
}
return;
}
const NSVisualEffectMaterial material = MaterialForVibrancyType(type);
if (!material)
return;
if (!vibrantView) {
NSView* contentView = _window.contentView;
vibrantView = [[NSVisualEffectView alloc] initWithFrame:contentView.bounds];
vibrantView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
vibrantView.blendingMode = NSVisualEffectBlendingModeBehindWindow;
vibrantView.state = state;
// Underneath everything else in the window.
[contentView addSubview:vibrantView
positioned:NSWindowBelow
relativeTo:nil];
_vibrantView = vibrantView;
if (animate) {
vibrantView.alphaValue = 0.0;
[NSAnimationContext
runAnimationGroup:^(NSAnimationContext* context) {
context.duration = durationMs / 1000.0;
vibrantView.animator.alphaValue = 1.0;
}
completionHandler:nil];
}
}
vibrantView.material = material;
}
- (void)performTabAction:(electron::mojom::AppShimTabAction)action {
using Action = electron::mojom::AppShimTabAction;
switch (action) {
case Action::kSelectPrevious:
[_window selectPreviousTab:nil];
break;
case Action::kSelectNext:
[_window selectNextTab:nil];
break;
case Action::kShowAll:
[_window toggleTabOverview:nil];
break;
case Action::kMergeAll:
[_window mergeAllWindows:nil];
break;
case Action::kMoveToNewWindow:
[_window moveTabToNewWindow:nil];
break;
case Action::kToggleTabBar:
[_window toggleTabBar:nil];
break;
}
}
- (void)addTabbedWindow:(ElectronAppShimWindow*)other {
NSWindow* otherWindow = other ? other->_window : nil;
if (otherWindow && otherWindow != _window)
[_window addTabbedWindow:otherWindow ordered:NSWindowAbove];
}
- (void)previewFile:(NSURL*)url title:(NSString*)title {
_previewItem = [[ElectronPreviewItem alloc] initWithURL:url title:title];
[_window makeKeyWindow];
QLPreviewPanel* panel = [QLPreviewPanel sharedPreviewPanel];
// The panel asks the responder chain who fills it only when it opens.
if (panel.visible)
[panel reloadData];
[panel makeKeyAndOrderFront:nil];
}
- (void)closeFilePreview {
// Asking for the shared panel makes one, which would show it.
if ([QLPreviewPanel sharedPreviewPanelExists] && _previewItem)
[[QLPreviewPanel sharedPreviewPanel] close];
_previewItem = nil;
}
- (BOOL)hasPreviewItem {
return _previewItem != nil;
}
// QLPreviewPanelDataSource
- (NSInteger)numberOfPreviewItemsInPreviewPanel:(QLPreviewPanel*)panel {
return _previewItem ? 1 : 0;
}
- (id<QLPreviewItem>)previewPanel:(QLPreviewPanel*)panel
previewItemAtIndex:(NSInteger)index {
return _previewItem;
}
@end
+2
View File
@@ -2138,6 +2138,8 @@ gin::ObjectTemplateBuilder App::GetObjectTemplateBuilder(v8::Isolate* isolate) {
.SetMethod("isInApplicationsFolder", &App::IsInApplicationsFolder)
.SetMethod("setActivationPolicy", &App::SetActivationPolicy)
.SetMethod("configureWebAuthn", &App::ConfigureWebAuthn)
.SetMethod("startAppShimServer", &App::StartAppShimServer)
.SetMethod("stopAppShimServer", &App::StopAppShimServer)
#endif
.SetMethod("setAboutPanelOptions",
base::BindRepeating(&Browser::SetAboutPanelOptions, browser))
+2
View File
@@ -256,6 +256,8 @@ class App final : public gin::Wrappable<App>,
#if BUILDFLAG(IS_MAC)
void SetActivationPolicy(gin_helper::ErrorThrower thrower,
const std::string& policy);
void StartAppShimServer(gin::Arguments* args);
void StopAppShimServer();
void ConfigureWebAuthn(gin_helper::ErrorThrower thrower,
gin::Arguments* args);
bool MoveToApplicationsFolder(gin_helper::ErrorThrower, gin::Arguments* args);
+38
View File
@@ -8,7 +8,13 @@
#include "base/path_service.h"
#include "base/strings/sys_string_conversions.h"
#include "electron/mas.h"
#include "gin/arguments.h"
#include "shell/browser/app_shim/app_shim_listener.h"
#include "shell/browser/browser.h"
#include "shell/common/app_shim_constants.h"
#include "shell/common/electron_paths.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/process_util.h"
#include "shell/common/thread_restrictions.h"
@@ -55,4 +61,36 @@ bool App::IsRunningUnderARM64Translation() const {
return proc_translated == 1;
}
void App::StartAppShimServer(gin::Arguments* args) {
#if IS_MAS_BUILD()
args->ThrowTypeError("App shims are not available in Mac App Store builds");
#else
if (!Browser::Get()->is_ready()) {
args->ThrowTypeError(
"app.startAppShimServer() can only be called after app is ready");
return;
}
std::string name = app_shim::kDefaultServerName;
if (args->Length() > 0) {
gin_helper::Dictionary options;
v8::Local<v8::Value> name_value;
if (!args->GetNext(&options) ||
(options.Get("name", &name_value) && !name_value->IsUndefined() &&
!options.Get("name", &name))) {
args->ThrowTypeError("Invalid options: name must be a string");
return;
}
}
std::string error;
if (!AppShimListener::Get()->Start(name, &error))
args->ThrowTypeError(error);
#endif
}
void App::StopAppShimServer() {
#if !IS_MAS_BUILD()
AppShimListener::Get()->Stop();
#endif
}
} // namespace electron::api
@@ -0,0 +1,117 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_APP_SHIM_HOST_H_
#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_APP_SHIM_HOST_H_
#include <mach/message.h>
#include <memory>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "electron/shell/common/app_shim.mojom.h"
#include "gin/wrappable.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "shell/browser/event_emitter_mixin.h"
#include "shell/browser/native_peer.h"
class GURL;
namespace gin {
class Arguments;
} // namespace gin
namespace gin_helper {
class ErrorThrower;
} // namespace gin_helper
namespace remote_cocoa {
class ApplicationHost;
} // namespace remote_cocoa
namespace electron {
class AppShimHostBootstrap;
namespace api {
class Menu;
// A connected app shim: another app, with a Dock tile of its own, that shows
// windows on this app's behalf. Windows created with it as their `appShim`
// option are NSWindows of the shim's process.
class AppShimHost final : public gin::Wrappable<AppShimHost>,
public gin_helper::EventEmitterMixin<AppShimHost> {
public:
// Takes over a shim's connection once its hello has been answered.
static AppShimHost* Create(
v8::Isolate* isolate,
std::unique_ptr<AppShimHostBootstrap> bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim);
// Public for cppgc::MakeGarbageCollected.
AppShimHost(std::unique_ptr<AppShimHostBootstrap> bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim);
~AppShimHost() override;
AppShimHost(const AppShimHost&) = delete;
AppShimHost& operator=(const AppShimHost&) = delete;
// The bundle that the process an audit token names runs from; empty when
// the process is gone.
static base::FilePath BundlePathForAuditToken(const audit_token_t& token);
// What creates this shim's NSWindows; null once the shim has disconnected.
remote_cocoa::ApplicationHost* GetRemoteCocoaApplicationHost() const;
// The shim's process; null once the shim has disconnected.
mojom::AppShim* GetAppShim() const;
// A window was made in the shim's process.
void DidCreateWindow();
// gin::Wrappable
static gin::WrapperInfo kWrapperInfo;
gin::ObjectTemplateBuilder GetObjectTemplateBuilder(
v8::Isolate* isolate) override;
const gin::WrapperInfo* wrapper_info() const override;
const char* GetHumanReadableName() const override;
const char* GetClassName() const { return "AppShimHost"; }
private:
class Connection;
// From the connection.
void OnReopen(bool has_visible_windows);
void OnFilesOpened(const std::vector<base::FilePath>& files);
void OnUrlsOpened(const std::vector<GURL>& urls);
// False when a listener prevented the default, which is to quit the shim.
bool OnQuitRequested();
void OnDisconnected();
// JS API.
int GetPid() const;
base::FilePath GetBundlePath() const;
std::string GetBundleId() const;
bool IsConnected() const;
bool SatisfiesCodeRequirement(gin_helper::ErrorThrower thrower,
const std::string& requirement);
void SetBadge(const std::string& label);
void Bounce(gin::Arguments* args);
void CancelBounce();
void Quit();
void SetApplicationMenu(gin::Arguments* args);
std::unique_ptr<Connection, NativePeerBase::Deleter> connection_;
};
} // namespace api
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_APP_SHIM_HOST_H_
@@ -0,0 +1,547 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/api/electron_api_app_shim_host.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/strings/utf_string_conversions.h"
#include "shell/browser/api/electron_api_menu.h"
#include "shell/browser/browser.h"
#include "shell/browser/native_window_mac_remote.h"
#include "shell/browser/ui/electron_menu_model.h"
#include "shell/browser/window_list.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/models/image_model.h"
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#include <bsm/libbsm.h>
#include <optional>
#include <string>
#include <utility>
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/functional/bind.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "components/remote_cocoa/browser/application_host.h"
#include "content/public/browser/browser_accessibility_state.h"
#include "content/public/browser/scoped_accessibility_mode.h"
#include "gin/arguments.h"
#include "gin/object_template_builder.h"
#include "mojo/public/cpp/bindings/pending_associated_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "shell/browser/app_shim/app_shim_listener.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_helper/error_thrower.h"
#include "shell/common/gin_helper/wrappable_pointer_tags.h"
#include "ui/accessibility/ax_mode.h"
#include "url/gurl.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/cppgc/persistent.h"
#include "v8/include/v8-cppgc.h"
namespace electron::api {
namespace {
// `rename` is the app's name and the shim's. The labels that the "about",
// "hide" and "quit" roles get by default name the app the menu was built in,
// and in a shim's menu bar they are about the shim.
std::vector<mojom::AppShimMenuItemPtr> DescribeMenu(
ElectronMenuModel* model,
const std::pair<std::u16string, std::u16string>& rename) {
std::vector<mojom::AppShimMenuItemPtr> items;
const size_t count = model->GetItemCount();
items.reserve(count);
for (size_t i = 0; i < count; ++i) {
auto item = mojom::AppShimMenuItem::New();
item->type = static_cast<int32_t>(model->GetTypeAt(i));
item->command_id = model->GetCommandIdAt(i);
item->label = model->GetLabelAt(i);
item->secondary_label = model->GetSecondaryLabelAt(i);
item->tool_tip = model->GetToolTipAt(i);
item->accessibility_label = model->GetAccessibilityLabelAt(i);
item->role = model->GetRoleAt(i);
if (!rename.first.empty() && !rename.second.empty() &&
(item->role == u"about" || item->role == u"hide" ||
item->role == u"quit") &&
item->label.ends_with(rename.first)) {
item->label.replace(item->label.size() - rename.first.size(),
rename.first.size(), rename.second);
}
item->custom_type = model->GetCustomTypeAt(i);
if (ui::ImageModel icon = model->GetIconAt(i); icon.IsImage())
item->icon = icon.GetImage().AsImageSkia();
ui::Accelerator accelerator;
if (model->GetAcceleratorAtWithParams(i, true, &accelerator)) {
item->has_accelerator = true;
item->accelerator_key_code = accelerator.key_code();
item->accelerator_modifiers = accelerator.modifiers();
item->accelerator_shifted_char = accelerator.shifted_char.value_or(0);
}
item->register_accelerator = model->ShouldRegisterAcceleratorAt(i);
item->works_when_hidden = model->WorksWhenHiddenAt(i);
item->checked = model->IsItemCheckedAt(i);
item->enabled = model->IsEnabledAt(i);
item->visible = model->IsVisibleAt(i);
item->group_id = model->GetGroupIdAt(i);
if (ElectronMenuModel* submenu = model->GetSubmenuModelAt(i))
item->submenu = DescribeMenu(submenu, rename);
items.push_back(std::move(item));
}
return items;
}
// The running code of the process an audit token names. Unlike a pid, a token
// cannot come to mean another process.
base::apple::ScopedCFTypeRef<SecCodeRef> CodeForAuditToken(
const audit_token_t& token) {
NSDictionary* attributes = @{
(__bridge NSString*)
kSecGuestAttributeAudit : [NSData dataWithBytes:&token length:sizeof(token)]
};
base::apple::ScopedCFTypeRef<SecCodeRef> code;
if (SecCodeCopyGuestWithAttributes(
nullptr, (__bridge CFDictionaryRef)attributes, kSecCSDefaultFlags,
code.InitializeInto()) != errSecSuccess) {
code.reset();
}
return code;
}
// The bundle the code runs from, or the executable's path for bare code.
base::FilePath PathOfCode(SecCodeRef code) {
base::apple::ScopedCFTypeRef<SecStaticCodeRef> static_code;
base::apple::ScopedCFTypeRef<CFURLRef> url;
if (!code ||
SecCodeCopyStaticCode(code, kSecCSDefaultFlags,
static_code.InitializeInto()) != errSecSuccess ||
SecCodeCopyPath(static_code.get(), kSecCSDefaultFlags,
url.InitializeInto()) != errSecSuccess) {
return {};
}
return base::apple::NSURLToFilePath((__bridge NSURL*)url.get());
}
} // namespace
// The native half of an AppShimHost: the Mojo connection to the shim process
// and the remote_cocoa application its windows are made through. It keeps the
// wrapper alive while the shim is connected, so a shim nobody holds on to in
// JavaScript still gets its events and keeps its windows.
class AppShimHost::Connection final : public NativePeer<AppShimHost>,
public mojom::AppShimHost {
public:
Connection(api::AppShimHost* wrapper,
std::unique_ptr<AppShimHostBootstrap> bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim)
: NativePeer<api::AppShimHost>(wrapper),
keep_alive_(wrapper),
audit_token_(bootstrap->audit_token()),
code_(CodeForAuditToken(audit_token_)),
bundle_path_(PathOfCode(code_.get())),
bootstrap_(std::move(bootstrap)),
receiver_(this, std::move(host_receiver)),
app_shim_(std::move(app_shim)) {
StartObservingShutdown();
if (NSBundle* bundle = [NSBundle
bundleWithPath:base::apple::FilePathToNSString(bundle_path_)]) {
bundle_id_ = base::SysNSStringToUTF8(bundle.bundleIdentifier);
NSString* name =
[bundle objectForInfoDictionaryKey:@"CFBundleDisplayName"]
?: [bundle objectForInfoDictionaryKey:@"CFBundleName"];
name_ = base::SysNSStringToUTF16(name);
}
receiver_.set_disconnect_handler(
base::BindOnce(&Connection::Disconnect, base::Unretained(this)));
app_shim_.set_disconnect_handler(
base::BindOnce(&Connection::Disconnect, base::Unretained(this)));
mojo::PendingAssociatedReceiver<remote_cocoa::mojom::Application>
application_receiver;
// Activation goes to the shim's process. Not to its bundle identifier,
// which is the shim's own claim and may be another app's.
application_host_ = std::make_unique<remote_cocoa::ApplicationHost>(
&application_receiver, std::string());
application_host_->set_pid(pid());
app_shim_->CreateRemoteCocoaApplication(std::move(application_receiver));
}
// The menu bar the shim shows while it is the active app. Without one it
// shows a short menu that it builds for itself.
void SetMenu(Menu* menu) {
menu_ = menu;
SendMenu();
}
Menu* menu() const { return menu_.Get(); }
void SendMenu() {
if (!connected())
return;
Menu* const menu = this->menu();
app_shim_->SetApplicationMenu(
menu ? std::make_optional(DescribeMenu(
menu->model(),
{base::UTF8ToUTF16(Browser::Get()->GetName()), name_}))
: std::nullopt);
}
bool connected() const { return connected_ && !quit_requested_; }
pid_t pid() const { return audit_token_to_pid(audit_token_); }
const base::FilePath& bundle_path() const { return bundle_path_; }
const std::string& bundle_id() const { return bundle_id_; }
SecCodeRef code() const { return code_.get(); }
remote_cocoa::ApplicationHost* application_host() const {
return quit_requested_ ? nullptr : application_host_.get();
}
mojom::AppShim* app_shim() const {
return connected_ ? app_shim_.get() : nullptr;
}
void DidCreateWindow() {
hosts_window_ = true;
ApplyAccessibilityMode();
}
// Ends the connection from JavaScript. The caller may be a listener of one
// of this shim's windows (its `show`, say), and closing that window under
// it would free what is notifying, so the windows close in a task of their
// own. Until then the shim reads as disconnected and takes no new window.
void Quit() {
if (!connected_ || quit_requested_)
return;
quit_requested_ = true;
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(&Connection::Disconnect, weak_factory_.GetWeakPtr()));
}
// Ends the connection, which ends the shim process and closes its windows.
void Disconnect() {
if (!TearDownConnection())
return;
if (auto shim_host = wrapper())
shim_host->OnDisconnected();
// May free the wrapper, and this with it, at the next garbage collection.
keep_alive_.Clear();
}
private:
~Connection() override = default;
void ApplyAccessibilityMode() {
if (!hosts_window_ || !wanted_accessibility_mode_ || !connected_)
return;
accessibility_mode_ =
content::BrowserAccessibilityState::GetInstance()
->CreateScopedModeForProcess(
(*wanted_accessibility_mode_ ==
mojom::AppShimAccessibilityMode::kComplete
? ui::kAXModeComplete
: ui::kAXModeBasic) |
ui::AXMode::kFromPlatform);
}
bool TearDownConnection() {
if (!connected_)
return false;
connected_ = false;
menu_.Clear();
weak_factory_.InvalidateWeakPtrs();
// Closes every window the shim hosts, synchronously: their closed events
// run JavaScript before this returns.
application_host_.reset();
accessibility_mode_.reset();
receiver_.reset();
app_shim_.reset();
bootstrap_.reset();
return true;
}
// NativePeerBase:
void TearDownNative() override {
TearDownConnection();
keep_alive_.Clear();
}
// mojom::AppShimHost:
void ReopenApp(bool has_visible_windows) override {
if (auto shim_host = wrapper())
shim_host->OnReopen(has_visible_windows);
}
void FilesOpened(const std::vector<base::FilePath>& files) override {
if (auto shim_host = wrapper())
shim_host->OnFilesOpened(files);
}
void UrlsOpened(const std::vector<GURL>& urls) override {
if (auto shim_host = wrapper())
shim_host->OnUrlsOpened(urls);
}
void EnableAccessibilitySupport(
mojom::AppShimAccessibilityMode mode) override {
// Any process of the same user can connect and ask for this, and it
// costs every page in the app. It waits until the app has given the
// shim a window, which is the app's way of saying it accepts the shim.
if (!wanted_accessibility_mode_ ||
mode == mojom::AppShimAccessibilityMode::kComplete) {
wanted_accessibility_mode_ = mode;
}
ApplyAccessibilityMode();
}
void MenuItemActivated(const std::vector<uint32_t>& path,
int32_t command_id,
int32_t event_flags) override {
// The shim's menu may be older than this one: the item is only chosen if
// what is at `path` now is still the item the shim meant.
Menu* const menu = this->menu();
ElectronMenuModel* model = menu ? menu->model() : nullptr;
for (size_t i = 0; model && i + 1 < path.size(); ++i) {
model = path[i] < model->GetItemCount()
? model->GetSubmenuModelAt(path[i])
: nullptr;
}
if (model && !path.empty() && path.back() < model->GetItemCount() &&
model->GetCommandIdAt(path.back()) == command_id) {
model->ActivatedAt(path.back(), event_flags);
}
// A click may have changed a check mark, or anything else.
SendMenu();
}
void MenuWillShow() override {
if (Menu* const menu = this->menu())
menu->model()->MenuWillShow();
SendMenu();
}
void MenuWillClose() override {
if (Menu* const menu = this->menu())
menu->model()->MenuWillClose();
}
void DidBecomeActive() override { SendMenu(); }
void NewWindowForTab(uint64_t window_id) override {
for (NativeWindow* window : WindowList::GetWindows()) {
if (!window->IsInAppShim())
continue;
auto* remote = static_cast<NativeWindowMacRemote*>(window);
if (remote->GetAppShim() == app_shim() &&
remote->GetRemoteWindowId() == window_id) {
remote->NotifyNewWindowForTab();
return;
}
}
}
void QuitRequested() override {
bool quit = true;
if (auto shim_host = wrapper())
quit = shim_host->OnQuitRequested();
if (!connected_)
return;
if (quit)
app_shim_->Quit();
else
app_shim_->CancelQuit();
}
cppgc::Persistent<api::AppShimHost> keep_alive_;
const audit_token_t audit_token_;
const base::apple::ScopedCFTypeRef<SecCodeRef> code_;
const base::FilePath bundle_path_;
std::string bundle_id_;
// What the shim calls itself, for its menu bar; its own claim.
std::u16string name_;
bool connected_ = true;
bool quit_requested_ = false;
// Holds the isolated connection the pipes below run over.
std::unique_ptr<AppShimHostBootstrap> bootstrap_;
mojo::Receiver<mojom::AppShimHost> receiver_;
mojo::Remote<mojom::AppShim> app_shim_;
std::unique_ptr<remote_cocoa::ApplicationHost> application_host_;
// Keeps accessibility on in this process while an assistive technology is
// looking at the shim.
// The shim's menu bar, if it has been given one.
cppgc::Persistent<Menu> menu_;
bool hosts_window_ = false;
std::optional<mojom::AppShimAccessibilityMode> wanted_accessibility_mode_;
std::unique_ptr<content::ScopedAccessibilityMode> accessibility_mode_;
base::WeakPtrFactory<Connection> weak_factory_{this};
};
gin::WrapperInfo AppShimHost::kWrapperInfo =
electron::MakeWrapperInfo(electron::kElectronAppShimHost);
// static
AppShimHost* AppShimHost::Create(
v8::Isolate* isolate,
std::unique_ptr<AppShimHostBootstrap> bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim) {
return cppgc::MakeGarbageCollected<AppShimHost>(
isolate->GetCppHeap()->GetAllocationHandle(), std::move(bootstrap),
std::move(host_receiver), std::move(app_shim));
}
AppShimHost::AppShimHost(
std::unique_ptr<AppShimHostBootstrap> bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim)
: connection_(
NativePeer<AppShimHost>::Create<Connection>(this,
std::move(bootstrap),
std::move(host_receiver),
std::move(app_shim))) {}
AppShimHost::~AppShimHost() = default;
// static
base::FilePath AppShimHost::BundlePathForAuditToken(
const audit_token_t& token) {
return PathOfCode(CodeForAuditToken(token).get());
}
remote_cocoa::ApplicationHost* AppShimHost::GetRemoteCocoaApplicationHost()
const {
return connection_->application_host();
}
mojom::AppShim* AppShimHost::GetAppShim() const {
return connection_->connected() ? connection_->app_shim() : nullptr;
}
void AppShimHost::DidCreateWindow() {
connection_->DidCreateWindow();
}
void AppShimHost::OnReopen(bool has_visible_windows) {
Emit("activate", has_visible_windows);
}
void AppShimHost::OnFilesOpened(const std::vector<base::FilePath>& files) {
for (const base::FilePath& file : files)
Emit("open-file", file);
}
void AppShimHost::OnUrlsOpened(const std::vector<GURL>& urls) {
for (const GURL& url : urls)
Emit("open-url", url.spec());
}
bool AppShimHost::OnQuitRequested() {
return !Emit("quit-requested");
}
void AppShimHost::OnDisconnected() {
Emit("disconnected");
}
int AppShimHost::GetPid() const {
return connection_->pid();
}
base::FilePath AppShimHost::GetBundlePath() const {
return connection_->bundle_path();
}
std::string AppShimHost::GetBundleId() const {
return connection_->bundle_id();
}
bool AppShimHost::IsConnected() const {
return connection_->connected();
}
bool AppShimHost::SatisfiesCodeRequirement(gin_helper::ErrorThrower thrower,
const std::string& requirement) {
base::apple::ScopedCFTypeRef<SecRequirementRef> requirement_ref;
if (SecRequirementCreateWithString(
base::SysUTF8ToCFStringRef(requirement).get(), kSecCSDefaultFlags,
requirement_ref.InitializeInto()) != errSecSuccess) {
thrower.ThrowError("Invalid code requirement");
return false;
}
return connection_->code() &&
SecCodeCheckValidity(connection_->code(), kSecCSDefaultFlags,
requirement_ref.get()) == errSecSuccess;
}
void AppShimHost::SetBadge(const std::string& label) {
if (mojom::AppShim* app_shim = connection_->app_shim())
app_shim->SetBadgeLabel(label);
}
void AppShimHost::Bounce(gin::Arguments* args) {
std::string type = "informational";
if (args->Length() > 0 && !args->GetNext(&type)) {
args->ThrowTypeError("Invalid bounce type");
return;
}
mojom::AppShimAttentionType attention_type;
if (type == "critical") {
attention_type = mojom::AppShimAttentionType::kCritical;
} else if (type == "informational") {
attention_type = mojom::AppShimAttentionType::kInformational;
} else {
args->ThrowTypeError("Invalid bounce type");
return;
}
if (mojom::AppShim* app_shim = connection_->app_shim())
app_shim->SetUserAttention(attention_type);
}
void AppShimHost::CancelBounce() {
if (mojom::AppShim* app_shim = connection_->app_shim())
app_shim->SetUserAttention(mojom::AppShimAttentionType::kCancel);
}
void AppShimHost::Quit() {
connection_->Quit();
}
void AppShimHost::SetApplicationMenu(gin::Arguments* args) {
v8::Local<v8::Value> value;
Menu* menu = nullptr;
if (!args->GetNext(&value) ||
(!value->IsNull() &&
(!gin::ConvertFromV8(args->isolate(), value, &menu) || !menu))) {
args->ThrowTypeError("Must pass a Menu or null");
return;
}
connection_->SetMenu(menu);
}
gin::ObjectTemplateBuilder AppShimHost::GetObjectTemplateBuilder(
v8::Isolate* isolate) {
return gin_helper::EventEmitterMixin<AppShimHost>::GetObjectTemplateBuilder(
isolate)
.SetProperty("pid", &AppShimHost::GetPid)
.SetProperty("bundlePath", &AppShimHost::GetBundlePath)
.SetProperty("bundleId", &AppShimHost::GetBundleId)
.SetMethod("isConnected", &AppShimHost::IsConnected)
.SetMethod("satisfiesCodeRequirement",
&AppShimHost::SatisfiesCodeRequirement)
.SetMethod("setBadge", &AppShimHost::SetBadge)
.SetMethod("bounce", &AppShimHost::Bounce)
.SetMethod("cancelBounce", &AppShimHost::CancelBounce)
.SetMethod("setApplicationMenu", &AppShimHost::SetApplicationMenu)
.SetMethod("quit", &AppShimHost::Quit);
}
const gin::WrapperInfo* AppShimHost::wrapper_info() const {
return &kWrapperInfo;
}
const char* AppShimHost::GetHumanReadableName() const {
return "Electron / AppShimHost";
}
} // namespace electron::api
+62 -2
View File
@@ -44,6 +44,10 @@
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#if BUILDFLAG(IS_MAC)
#include "shell/browser/api/electron_api_app_shim_host.h"
#endif
#if defined(TOOLKIT_VIEWS)
#include "shell/browser/native_window_views.h"
#endif
@@ -890,7 +894,13 @@ void BaseWindow::SetParentWindow(v8::Local<v8::Value> value,
if (value->IsNull() || value->IsUndefined()) {
parent_window_.Reset();
window_->SetParentWindow(nullptr);
} else if (window_->IsInAppShim()) {
args->ThrowTypeError("A window in an app shim can't have a parent window");
} else if (gin::ConvertFromV8(isolate(), value, &parent)) {
if (parent->window() && parent->window()->IsInAppShim()) {
args->ThrowTypeError("A window in an app shim can't be a parent window");
return;
}
parent_window_.Reset(isolate(), parent.ToV8());
window_->SetParentWindow(parent->window());
} else {
@@ -1045,9 +1055,13 @@ void BaseWindow::ToggleTabBar() {
void BaseWindow::AddTabbedWindow(NativeWindow* const window,
gin::Arguments* const args) {
if (!window_->AddTabbedWindow(window))
if (!window_->AddTabbedWindow(window)) {
args->ThrowTypeError(
"AddTabbedWindow cannot be called by a window on itself.");
window_.get() != window &&
(window_->IsInAppShim() || window->IsInAppShim())
? "AddTabbedWindow needs both windows to be in the same app"
: "AddTabbedWindow cannot be called by a window on itself.");
}
}
v8::Local<v8::Value> BaseWindow::GetTabbingIdentifier() {
@@ -1301,6 +1315,11 @@ gin_helper::WrappableBase* BaseWindow::New(gin::Arguments* const args) {
args->ThrowTypeError(error_message);
return nullptr;
}
if (!IsAppShimValid(options, &error_message) ||
!IsParentValid(options, &error_message)) {
args->ThrowTypeError(error_message);
return nullptr;
}
return new BaseWindow(args, options);
}
@@ -1327,6 +1346,47 @@ bool BaseWindow::IsWindowNameValid(const gin_helper::Dictionary& options,
return true;
}
// static
bool BaseWindow::IsAppShimValid(const gin_helper::Dictionary& options,
std::string* error_message) {
#if BUILDFLAG(IS_MAC)
v8::Local<v8::Value> value;
if (!options.Get(options::kAppShim, &value) || value->IsNullOrUndefined())
return true;
AppShimHost* app_shim = nullptr;
if (!gin::ConvertFromV8(options.isolate(), value, &app_shim) || !app_shim) {
*error_message = "appShim must be an AppShimHost";
return false;
}
if (!app_shim->GetRemoteCocoaApplicationHost()) {
*error_message = "The app shim has disconnected";
return false;
}
if (bool modal = false; options.Get(options::kModal, &modal) && modal) {
*error_message = "A window in an app shim can't be modal";
return false;
}
if (v8::Local<v8::Value> parent;
options.Get("parent", &parent) && !parent->IsNullOrUndefined()) {
*error_message = "A window in an app shim can't have a parent window";
return false;
}
#endif
return true;
}
// static
bool BaseWindow::IsParentValid(const gin_helper::Dictionary& options,
std::string* error_message) {
gin_helper::Handle<BaseWindow> parent;
if (options.Get("parent", &parent) && !parent.IsEmpty() && parent->window() &&
parent->window()->IsInAppShim()) {
*error_message = "A window in an app shim can't be a parent window";
return false;
}
return true;
}
// static
v8::Local<v8::FunctionTemplate> BaseWindow::GetConstructorTemplate(
v8::Isolate* isolate) {
@@ -62,6 +62,14 @@ class BaseWindow : public gin_helper::TrackableObject<BaseWindow>,
static bool IsWindowNameValid(const gin_helper::Dictionary& options,
std::string* error_message);
// False when the options name an app shim (macOS) that can't host a window.
static bool IsAppShimValid(const gin_helper::Dictionary& options,
std::string* error_message);
// Returns false, with the reason, when the `parent` in `options` is a
// window that can't have children.
static bool IsParentValid(const gin_helper::Dictionary& options,
std::string* error_message);
const NativeWindow* window() const { return window_.get(); }
NativeWindow* window() { return window_.get(); }
@@ -375,6 +375,11 @@ gin_helper::WrappableBase* BrowserWindow::New(gin_helper::ErrorThrower thrower,
thrower.ThrowError(error_message);
return nullptr;
}
if (!IsAppShimValid(options, &error_message) ||
!IsParentValid(options, &error_message)) {
thrower.ThrowTypeError(error_message);
return nullptr;
}
return new BrowserWindow(args, options);
}
@@ -9,6 +9,7 @@
#include "ui/base/cocoa/nsmenu_additions.h"
#include "ui/base/cocoa/nsmenuitem_additions.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/views/cocoa/native_widget_mac_ns_window_host.h"
#import <Cocoa/Cocoa.h>
@@ -24,6 +25,13 @@ bool WebContents::IsFocused() const {
return false;
if (type() != Type::kBackgroundPage) {
// A window in an app shim is key in the shim's process; its stand-in in
// this process never is.
if (auto* host = views::NativeWidgetMacNSWindowHost::GetFromNativeWindow(
web_contents()->GetTopLevelNativeWindow());
host && host->application_host()) {
return host->IsWindowKey() && view->HasFocus();
}
auto window = [web_contents()->GetNativeView().GetNativeNSView() window];
// On Mac the render widget host view does not lose focus when the window
// loses focus so check if the top level window is the key window.
@@ -45,6 +53,15 @@ bool WebContents::PlatformHandleKeyboardEvent(
return false;
NSEvent* ns_event = event.os_event.Get();
// A window in an app shim: the key was pressed in the shim's process, and
// the menu it is a shortcut of is the shim's, not this app's.
if (auto* host = views::NativeWidgetMacNSWindowHost::GetFromNativeWindow(
source->GetTopLevelNativeWindow());
host && host->application_host()) {
return host->RedispatchKeyEvent(ns_event);
}
// Send the event to the menu before sending it to the window
if (ns_event.type == NSEventTypeKeyDown) {
// If the keyboard event is a system shortcut, it's already sent to the
+128
View File
@@ -0,0 +1,128 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_APP_SHIM_APP_SHIM_LISTENER_H_
#define ELECTRON_SHELL_BROWSER_APP_SHIM_APP_SHIM_LISTENER_H_
#include <bsm/libbsm.h>
#include <memory>
#include <set>
#include <string>
#include "base/apple/scoped_mach_port.h"
#include "base/containers/unique_ptr_adapters.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/no_destructor.h"
#include "base/timer/timer.h"
#include "electron/shell/common/app_shim.mojom.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/platform/platform_channel_endpoint.h"
#include "mojo/public/cpp/platform/platform_channel_server_endpoint.h"
#include "mojo/public/cpp/system/isolated_connection.h"
namespace electron {
// The first interface a shim talks to, on the channel it handed the listener.
// The listener owns it until the shim says hello, and drops it if the shim
// goes away or takes too long first. After the hello it belongs to whoever
// serves the shim.
class AppShimHostBootstrap : public mojom::AppShimHostBootstrap {
public:
class Client {
public:
// The shim said hello. Returning false turns it away.
virtual bool OnShimConnected(
AppShimHostBootstrap* bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim) = 0;
// The shim went away, or timed out, before it said hello.
virtual void OnBootstrapAbandoned(AppShimHostBootstrap* bootstrap) = 0;
protected:
virtual ~Client() = default;
};
// `audit_token` identifies the process that sent `endpoint`. Null when
// `endpoint` is not usable. `client` must outlive the result.
static std::unique_ptr<AppShimHostBootstrap> CreateForChannel(
mojo::PlatformChannelEndpoint endpoint,
audit_token_t audit_token,
Client* client);
~AppShimHostBootstrap() override;
AppShimHostBootstrap(const AppShimHostBootstrap&) = delete;
AppShimHostBootstrap& operator=(const AppShimHostBootstrap&) = delete;
const audit_token_t& audit_token() const { return audit_token_; }
private:
AppShimHostBootstrap(audit_token_t audit_token, Client* client);
void Abandon();
// mojom::AppShimHostBootstrap:
void OnShimConnected(mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
OnShimConnectedCallback callback) override;
const audit_token_t audit_token_;
// Null once the shim has said hello.
raw_ptr<Client> client_;
base::OneShotTimer hello_timer_;
mojo::IsolatedConnection connection_;
mojo::Receiver<mojom::AppShimHostBootstrap> receiver_{this};
};
// Publishes a Mach bootstrap server that app shims connect to, and hands each
// shim that connects to the `app` module. Lives on the UI thread.
class AppShimListener : private AppShimHostBootstrap::Client {
public:
static AppShimListener* Get();
AppShimListener(const AppShimListener&) = delete;
AppShimListener& operator=(const AppShimListener&) = delete;
// Starts listening as "<base bundle id>.<name>". False, with `error` set,
// when the name is malformed or another process holds it.
bool Start(const std::string& name, std::string* error);
// Stops listening and drops the shims that have not said hello yet.
void Stop();
bool is_listening() const { return !!acceptor_; }
private:
friend class base::NoDestructor<AppShimListener>;
class Acceptor;
AppShimListener();
~AppShimListener() override;
void OnClientConnected(mojo::PlatformChannelEndpoint endpoint,
audit_token_t audit_token);
// A shim that loaded another build of Electron than this one gave up. It
// waits for `reply` to be let go of before it quits.
void OnBuildMismatch(audit_token_t audit_token,
base::apple::ScopedMachSendRight reply);
// AppShimHostBootstrap::Client:
bool OnShimConnected(AppShimHostBootstrap* bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim) override;
void OnBootstrapAbandoned(AppShimHostBootstrap* bootstrap) override;
// Published for shims to look up and never read from.
mojo::PlatformChannelServerEndpoint build_marker_;
std::unique_ptr<Acceptor> acceptor_;
// Shims that have connected and not said hello yet.
std::set<std::unique_ptr<AppShimHostBootstrap>, base::UniquePtrComparator>
pending_bootstraps_;
base::WeakPtrFactory<AppShimListener> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_APP_SHIM_APP_SHIM_LISTENER_H_
+315
View File
@@ -0,0 +1,315 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/app_shim/app_shim_listener.h"
#include <mach/message.h>
#include <algorithm>
#include <utility>
#include "base/apple/dispatch_source.h"
#include "base/apple/foundation_util.h"
#include "base/apple/mach_logging.h"
#include "base/apple/scoped_mach_port.h"
#include "base/functional/bind.h"
#include "base/mac/scoped_mach_msg_destroy.h"
#include "base/memory/ptr_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/task/sequenced_task_runner.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "mojo/public/cpp/platform/named_platform_channel.h"
#include "mojo/public/cpp/platform/platform_channel_server_endpoint.h"
#include "shell/browser/api/electron_api_app.h"
#include "shell/browser/api/electron_api_app_shim_host.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/app_shim_constants.h"
#include "shell/common/gin_converters/file_path_converter.h"
namespace electron {
namespace {
// How long a process that connected may take to say hello.
constexpr base::TimeDelta kHelloTimeout = base::Seconds(30);
// The longest name the bootstrap server takes (BOOTSTRAP_MAX_NAME_LEN).
constexpr size_t kMaxServerNameLength = 127;
} // namespace
// AppShimHostBootstrap ------------------------------------------------------
// static
std::unique_ptr<AppShimHostBootstrap> AppShimHostBootstrap::CreateForChannel(
mojo::PlatformChannelEndpoint endpoint,
audit_token_t audit_token,
Client* client) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!endpoint.platform_handle().is_mach_send())
return nullptr;
auto bootstrap =
base::WrapUnique(new AppShimHostBootstrap(audit_token, client));
mojo::ScopedMessagePipeHandle message_pipe =
bootstrap->connection_.Connect(std::move(endpoint));
bootstrap->receiver_.Bind(mojo::PendingReceiver<mojom::AppShimHostBootstrap>(
std::move(message_pipe)));
bootstrap->receiver_.set_disconnect_handler(base::BindOnce(
&AppShimHostBootstrap::Abandon, base::Unretained(bootstrap.get())));
bootstrap->hello_timer_.Start(FROM_HERE, kHelloTimeout, bootstrap.get(),
&AppShimHostBootstrap::Abandon);
return bootstrap;
}
AppShimHostBootstrap::AppShimHostBootstrap(audit_token_t audit_token,
Client* client)
: audit_token_(audit_token), client_(client) {}
AppShimHostBootstrap::~AppShimHostBootstrap() = default;
void AppShimHostBootstrap::Abandon() {
// After the hello the shim closing its bootstrap pipe is expected.
if (Client* client = std::exchange(client_, nullptr))
client->OnBootstrapAbandoned(this); // Deletes this.
}
void AppShimHostBootstrap::OnShimConnected(
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
OnShimConnectedCallback callback) {
hello_timer_.Stop();
// One hello per channel.
Client* client = std::exchange(client_, nullptr);
if (!client) {
std::move(callback).Run(mojo::NullReceiver());
return;
}
mojo::PendingRemote<mojom::AppShim> app_shim;
mojo::PendingReceiver<mojom::AppShim> app_shim_receiver =
app_shim.InitWithNewPipeAndPassReceiver();
// A client that accepts takes ownership of this.
if (client->OnShimConnected(this, std::move(host_receiver),
std::move(app_shim))) {
std::move(callback).Run(std::move(app_shim_receiver));
return;
}
std::move(callback).Run(mojo::NullReceiver());
client->OnBootstrapAbandoned(this); // Deletes this.
}
// AppShimListener::Acceptor -------------------------------------------------
// Receives the raw Mach messages shims send to the bootstrap server. Messages
// arrive on a dispatch queue; each connection is passed on to the UI thread.
class AppShimListener::Acceptor {
public:
using ConnectCallback =
base::RepeatingCallback<void(mojo::PlatformChannelEndpoint,
audit_token_t)>;
using BuildMismatchCallback =
base::RepeatingCallback<void(audit_token_t,
base::apple::ScopedMachSendRight)>;
Acceptor(mojo::PlatformChannelServerEndpoint endpoint,
const std::string& server_name,
ConnectCallback on_connect,
BuildMismatchCallback on_build_mismatch)
: endpoint_(std::move(endpoint)),
on_connect_(std::move(on_connect)),
on_build_mismatch_(std::move(on_build_mismatch)),
ui_task_runner_(content::GetUIThreadTaskRunner({})) {
dispatch_source_ = std::make_unique<base::apple::DispatchSource>(
server_name.c_str(), port(), ^{
HandleRequest();
});
dispatch_source_->Resume();
}
~Acceptor() {
// Waits for a running HandleRequest.
dispatch_source_.reset();
}
private:
mach_port_t port() const {
return endpoint_.platform_handle().GetMachReceiveRight().get();
}
void HandleRequest() {
struct : mach_msg_base_t {
mach_msg_audit_trailer_t trailer;
} request{};
request.header.msgh_size = sizeof(request);
request.header.msgh_local_port = port();
kern_return_t kr = mach_msg(
&request.header,
MACH_RCV_MSG | MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) |
MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT),
0, sizeof(request), port(), MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_msg";
return;
}
base::ScopedMachMsgDestroy scoped_message(&request.header);
if (request.header.msgh_size != sizeof(mach_msg_base_t))
return;
if (request.header.msgh_id == app_shim::kBuildMismatchMsgId) {
// The reply right is what the shim waits on; it is let go of once the
// shim has been identified, which needs its process to be there.
base::apple::ScopedMachSendRight reply(request.header.msgh_remote_port);
scoped_message.Disarm();
ui_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(on_build_mismatch_, request.trailer.msgh_audit,
std::move(reply)));
return;
}
if (request.header.msgh_id != app_shim::kBootstrapMsgId)
return;
mojo::PlatformChannelEndpoint remote_endpoint(mojo::PlatformHandle(
base::apple::ScopedMachSendRight(request.header.msgh_remote_port)));
if (!remote_endpoint.is_valid())
return;
scoped_message.Disarm();
ui_task_runner_->PostTask(
FROM_HERE, base::BindOnce(on_connect_, std::move(remote_endpoint),
request.trailer.msgh_audit));
}
mojo::PlatformChannelServerEndpoint endpoint_;
const ConnectCallback on_connect_;
const BuildMismatchCallback on_build_mismatch_;
const scoped_refptr<base::SequencedTaskRunner> ui_task_runner_;
std::unique_ptr<base::apple::DispatchSource> dispatch_source_;
};
// AppShimListener -----------------------------------------------------------
// static
AppShimListener* AppShimListener::Get() {
static base::NoDestructor<AppShimListener> listener;
return listener.get();
}
AppShimListener::AppShimListener() = default;
AppShimListener::~AppShimListener() = default;
bool AppShimListener::Start(const std::string& name, std::string* error) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (acceptor_) {
*error = "The app shim server is already started";
return false;
}
const bool valid = !name.empty() && std::ranges::all_of(name, [](char c) {
return base::IsAsciiAlphaNumeric(c) || c == '.' || c == '-' || c == '_';
});
if (!valid) {
*error = "The app shim server name may only hold letters, digits, '.', "
"'-' and '_'";
return false;
}
mojo::NamedPlatformChannel::Options options;
options.server_name = base::StrCat({base::apple::BaseBundleID(), ".", name});
if (options.server_name.size() > kMaxServerNameLength) {
*error = base::StrCat(
{"The app shim server name \"", options.server_name, "\" is too long"});
return false;
}
// The marker goes up first, so that a shim that finds the server finds the
// marker too. See app_shim::BuildMarkerServerName().
mojo::NamedPlatformChannel::Options marker_options;
marker_options.server_name =
app_shim::BuildMarkerServerName(options.server_name);
if (marker_options.server_name.size() > kMaxServerNameLength) {
*error = base::StrCat(
{"The app shim server name \"", options.server_name, "\" is too long"});
return false;
}
mojo::PlatformChannelServerEndpoint marker =
mojo::NamedPlatformChannel(marker_options).TakeServerEndpoint();
mojo::NamedPlatformChannel channel(options);
mojo::PlatformChannelServerEndpoint endpoint = channel.TakeServerEndpoint();
if (!marker.is_valid() || !endpoint.is_valid()) {
*error = base::StrCat({"Could not publish the app shim server \"",
options.server_name,
"\"; another process may already hold that name"});
return false;
}
build_marker_ = std::move(marker);
acceptor_ = std::make_unique<Acceptor>(
std::move(endpoint), options.server_name,
base::BindRepeating(&AppShimListener::OnClientConnected,
weak_factory_.GetWeakPtr()),
base::BindRepeating(&AppShimListener::OnBuildMismatch,
weak_factory_.GetWeakPtr()));
return true;
}
void AppShimListener::OnBuildMismatch(audit_token_t audit_token,
base::apple::ScopedMachSendRight reply) {
if (!is_listening() || Browser::Get()->is_shutting_down())
return;
const base::FilePath bundle_path =
api::AppShimHost::BundlePathForAuditToken(audit_token);
// The shim may quit now, and need not wait for what listeners do.
reply.reset();
v8::Isolate* const isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
api::App::Get()->Emit("app-shim-version-mismatch", bundle_path);
}
void AppShimListener::OnClientConnected(mojo::PlatformChannelEndpoint endpoint,
audit_token_t audit_token) {
if (!is_listening() || Browser::Get()->is_shutting_down())
return;
if (auto bootstrap = AppShimHostBootstrap::CreateForChannel(
std::move(endpoint), audit_token, this)) {
pending_bootstraps_.insert(std::move(bootstrap));
}
}
void AppShimListener::Stop() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
acceptor_.reset();
build_marker_.reset();
pending_bootstraps_.clear();
}
bool AppShimListener::OnShimConnected(
AppShimHostBootstrap* bootstrap,
mojo::PendingReceiver<mojom::AppShimHost> host_receiver,
mojo::PendingRemote<mojom::AppShim> app_shim) {
auto it = pending_bootstraps_.find(bootstrap);
if (it == pending_bootstraps_.end() || !is_listening() ||
Browser::Get()->is_shutting_down()) {
return false;
}
std::unique_ptr<AppShimHostBootstrap> owned =
std::move(pending_bootstraps_.extract(it).value());
v8::Isolate* const isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
api::AppShimHost* shim_host = api::AppShimHost::Create(
isolate, std::move(owned), std::move(host_receiver), std::move(app_shim));
v8::Local<v8::Object> wrapper;
if (shim_host->GetWrapper(isolate).ToLocal(&wrapper))
api::App::Get()->Emit("app-shim-connected", wrapper);
return true;
}
void AppShimListener::OnBootstrapAbandoned(AppShimHostBootstrap* bootstrap) {
auto it = pending_bootstraps_.find(bootstrap);
if (it != pending_bootstraps_.end())
pending_bootstraps_.erase(it);
}
} // namespace electron
+4
View File
@@ -336,6 +336,10 @@ bool NativeWindow::IsClosed() const {
return is_closed_;
}
bool NativeWindow::IsInAppShim() const {
return false;
}
void NativeWindow::SetSize(const gfx::Size& size, bool animate) {
SetBounds(gfx::Rect(GetPosition(), size), animate);
}
+4
View File
@@ -83,6 +83,10 @@ class NativeWindow : public views::WidgetDelegate {
virtual void Close() = 0;
virtual void CloseImmediately() = 0;
virtual bool IsClosed() const;
// macOS: whether the window's NSWindow is in an app shim's process. What
// GetNativeWindow() names is then a stand-in that is never on screen, and
// nothing may be attached to it.
virtual bool IsInAppShim() const;
virtual void Focus(bool focus) = 0;
virtual bool IsFocused() const = 0;
virtual void Show() = 0;
+11 -44
View File
@@ -22,6 +22,7 @@
#include "content/public/browser/desktop_media_id.h"
#include "shell/browser/browser.h"
#include "shell/browser/javascript_environment.h"
#include "shell/browser/native_window_mac_remote.h"
#include "shell/browser/ui/cocoa/electron_native_widget_mac.h"
#include "shell/browser/ui/cocoa/electron_ns_panel.h"
#include "shell/browser/ui/cocoa/electron_ns_window.h"
@@ -31,6 +32,7 @@
#include "shell/browser/ui/cocoa/root_view_mac.h"
#include "shell/browser/ui/cocoa/window_buttons_proxy.h"
#include "shell/browser/ui/drag_util.h"
#include "shell/browser/ui/views/content_view_targeter_delegate.h"
#include "shell/browser/window_list.h"
#include "shell/common/gin_converters/gfx_converter.h"
#include "shell/common/gin_helper/dictionary.h"
@@ -44,7 +46,6 @@
#include "ui/views/background.h"
#include "ui/views/cocoa/native_widget_mac_ns_window_host.h"
#include "ui/views/view_targeter.h"
#include "ui/views/view_targeter_delegate.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/native_frame_view_mac.h"
@@ -112,49 +113,6 @@ struct Converter<electron::NativeWindowMac::VisualEffectState> {
} // namespace gin
namespace {
// A ViewTargeterDelegate installed on content_view_ that makes it
// hit-test-transparent except where one of its descendants covers the
// target rect.
//
// In BrowserWindow, the WebContentsView is added as a sibling of
// content_view_ at a lower z-order so that user-added child views paint
// above web content. content_view_ itself covers the full window, so the
// default DoesIntersectRect (a bounds check) would always return true and
// the parent targeter would stop there, never reaching the WebContentsView
// behind it. By only intersecting where a child actually does, the parent's
// default TargetForRect loop skips content_view_ and falls through to the
// sibling, ultimately resolving to the WebContentsView's NativeViewHost so
// BridgedContentView::hitTest: routes the event to RenderWidgetHostViewCocoa.
//
// This must never affect TargetForRect's contract — it only narrows
// HitTestRect, which the default loop already handles by skipping the child.
// Returning nullptr from TargetForRect (the previous approach) violated the
// contract and crashed RootView::UpdateCursor (see #51576).
class ContentViewTargeterDelegate : public views::ViewTargeterDelegate {
public:
bool DoesIntersectRect(const views::View* target,
const gfx::Rect& rect) const override {
if (!views::ViewTargeterDelegate::DoesIntersectRect(target, rect)) {
return false;
}
for (const views::View* child : target->children()) {
if (!child->GetVisible() || !child->GetCanProcessEventsWithinSubtree()) {
continue;
}
gfx::RectF rect_in_child(rect);
views::View::ConvertRectToTarget(target, child, &rect_in_child);
if (child->HitTestRect(gfx::ToEnclosingRect(rect_in_child))) {
return true;
}
}
return false;
}
};
} // namespace
namespace electron {
NativeWindowMac::~NativeWindowMac() = default;
@@ -1621,6 +1579,10 @@ void NativeWindowMac::ToggleTabBar() {
}
bool NativeWindowMac::AddTabbedWindow(NativeWindow* window) {
// A window in an app shim is another process's: only a stand-in for it is
// here.
if (window->IsInAppShim())
return false;
if (window_ == window->GetNativeWindow().GetNativeNSWindow()) {
return false;
} else {
@@ -1928,6 +1890,11 @@ std::unique_ptr<NativeWindow> NativeWindow::Create(
const int32_t base_window_id,
const gin_helper::Dictionary& options,
NativeWindow* parent) {
if (remote_cocoa::ApplicationHost* application_host =
NativeWindowMacRemote::ApplicationHostFromOptions(options)) {
return std::make_unique<NativeWindowMacRemote>(base_window_id, options,
parent, application_host);
}
return std::make_unique<NativeWindowMac>(base_window_id, options, parent);
}
+267
View File
@@ -0,0 +1,267 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_NATIVE_WINDOW_MAC_REMOTE_H_
#define ELECTRON_SHELL_BROWSER_NATIVE_WINDOW_MAC_REMOTE_H_
#import <Cocoa/Cocoa.h>
#include <memory>
#include <optional>
#include <string>
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "electron/shell/common/app_shim.mojom-forward.h"
#include "shell/browser/native_window.h"
#include "shell/browser/ui/file_dialog.h"
#include "shell/browser/ui/message_box.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/views/widget/widget_observer.h"
#include "v8/include/cppgc/persistent.h"
namespace electron::api {
class AppShimHost;
} // namespace electron::api
namespace remote_cocoa {
class ApplicationHost;
namespace mojom {
class NativeWidgetNSWindow;
} // namespace mojom
} // namespace remote_cocoa
namespace electron {
class RootViewMac;
// A window whose NSWindow lives in an app shim process (see
// api::AppShimHost), so the Dock, Mission Control and the window server
// attribute it to the shim instead of this app. There is no NSWindow of it in
// this process to message, so everything goes through views::Widget, which
// forwards to the shim over remote_cocoa's Mojo interfaces. What those
// interfaces have no call for is not supported and is a no-op here; see
// docs/api/app-shim-host.md for the list.
class NativeWindowMacRemote : public NativeWindow,
private views::WidgetObserver {
public:
// The application host of the options' `appShim`, or null when there is
// none or it has disconnected.
static remote_cocoa::ApplicationHost* ApplicationHostFromOptions(
const gin_helper::Dictionary& options);
NativeWindowMacRemote(int32_t base_window_id,
const gin_helper::Dictionary& options,
NativeWindow* parent,
remote_cocoa::ApplicationHost* application_host);
~NativeWindowMacRemote() override;
// NativeWindow:
void OnTitleChanged() override;
void SetContentView(views::View* view) override;
void Close() override;
void CloseImmediately() override;
void Focus(bool focus) override;
bool IsFocused() const override;
void Show() override;
void ShowInactive() override;
void Hide() override;
bool IsVisible() const override;
bool IsEnabled() const override;
void SetEnabled(bool enable) override;
void Maximize() override;
void Unmaximize() override;
bool IsMaximized() const override;
void Minimize() override;
void Restore() override;
bool IsMinimized() const override;
void SetFullScreen(bool fullscreen) override;
bool IsFullscreen() const override;
void SetBounds(const gfx::Rect& bounds, bool animate) override;
gfx::Rect GetBounds() const override;
gfx::Rect GetNormalBounds() const override;
void SetSizeConstraints(
const extensions::SizeConstraints& window_constraints) override;
void SetContentSizeConstraints(
const extensions::SizeConstraints& size_constraints) override;
void SetResizable(bool resizable) override;
bool MoveAbove(const std::string& sourceId) override;
void MoveTop() override;
bool IsResizable() const override;
void SetMovable(bool movable) override;
bool IsMovable() const override;
void SetMinimizable(bool minimizable) override;
bool IsMinimizable() const override;
void SetMaximizable(bool maximizable) override;
bool IsMaximizable() const override;
void SetFullScreenable(bool fullscreenable) override;
bool IsFullScreenable() const override;
void SetClosable(bool closable) override;
bool IsClosable() const override;
void SetAlwaysOnTop(ui::ZOrderLevel z_order,
const std::string& level,
int relative_level) override;
std::string GetAlwaysOnTopLevel() const override;
ui::ZOrderLevel GetZOrderLevel() const override;
void Center() override;
void Invalidate() override;
bool IsActive() const override;
void SetActive(bool is_key) override;
void RemoveChildFromParentWindow() override;
void RemoveChildWindow(NativeWindow* child) override;
void AttachChildren() override;
void DetachChildren() override;
void FlashFrame(bool flash) override;
void SetSkipTaskbar(bool skip) override;
void SetExcludedFromShownWindowsMenu(bool excluded) override;
bool IsExcludedFromShownWindowsMenu() const override;
void SetSimpleFullScreen(bool simple_fullscreen) override;
bool IsSimpleFullScreen() const override;
void SetKiosk(bool kiosk) override;
bool IsKiosk() const override;
void SetBackgroundColor(SkColor color) override;
SkColor GetBackgroundColor() const override;
void SetHasShadow(bool has_shadow) override;
bool HasShadow() const override;
void SetOpacity(const double opacity) override;
double GetOpacity() const override;
void SetIgnoreMouseEvents(bool ignore, bool forward) override;
void SetContentProtection(bool enable) override;
bool IsContentProtected() const override;
content::DesktopMediaID GetDesktopMediaID() const override;
gfx::NativeView GetNativeView() const override;
gfx::NativeWindow GetNativeWindow() const override;
gfx::AcceleratedWidget GetAcceleratedWidget() const override;
NativeWindowHandle GetNativeWindowHandle() const override;
void SetProgressBar(double progress, const ProgressState state) override;
void SetOverlayIcon(const gfx::Image& overlay,
const std::string& description) override;
void SetVisibleOnAllWorkspaces(bool visible,
bool visibleOnFullScreen,
bool skipTransformProcessType) override;
bool IsVisibleOnAllWorkspaces() const override;
void SetWindowButtonVisibility(bool visible) override;
bool GetWindowButtonVisibility() const override;
void SetWindowButtonPosition(std::optional<gfx::Point> position) override;
std::optional<gfx::Point> GetWindowButtonPosition() const override;
void RedrawTrafficLights() override;
void SetVibrancy(const std::string& type, int duration) override;
void SelectPreviousTab() override;
void SelectNextTab() override;
void ShowAllTabs() override;
void MergeAllWindows() override;
void MoveTabToNewWindow() override;
void ToggleTabBar() override;
bool AddTabbedWindow(NativeWindow* window) override;
std::optional<std::string> GetTabbingIdentifier() const override;
void PreviewFile(const std::string& path,
const std::string& display_name) override;
void CloseFilePreview() override;
void UpdateFrame() override;
bool IsHiddenInMissionControl() const override;
void SetHiddenInMissionControl(bool hidden) override;
void SetAspectRatio(double aspect_ratio,
const gfx::Size& extra_size) override;
gfx::Rect ContentBoundsToWindowBounds(const gfx::Rect& bounds) const override;
gfx::Rect WindowBoundsToContentBounds(const gfx::Rect& bounds) const override;
// The shim's process, and the id it knows this window by; null and 0 once
// either is gone.
mojom::AppShim* GetAppShim() const;
uint64_t GetRemoteWindowId() const;
// Dialogs with this window as their parent. They are sheets of the
// NSWindow in the shim's process, which shows them; the callbacks run with
// "cancelled" when the shim or the window goes away first.
void ShowMessageBox(const MessageBoxSettings& settings,
MessageBoxCallback callback);
// Runs a nested loop until the box is answered.
int ShowMessageBoxSync(const MessageBoxSettings& settings);
// False when no window in a shim is showing a box with this id.
static bool CloseMessageBox(int id);
void ShowOpenDialog(const file_dialog::DialogSettings& settings,
file_dialog::OpenDialogCallback callback);
void ShowSaveDialog(const file_dialog::DialogSettings& settings,
file_dialog::SaveDialogCallback callback);
// The window's close button, once the shim has its answer (see the client
// view).
protected:
// views::WidgetDelegate:
views::View* GetContentsView() override;
views::ClientView* CreateClientView(views::Widget* widget) override;
std::unique_ptr<views::FrameView> CreateFrameView(
views::Widget* widget) override;
private:
// views::WidgetObserver:
void OnWidgetActivationChanged(views::Widget* widget, bool active) override;
void OnWidgetBoundsChanged(views::Widget* widget,
const gfx::Rect& bounds) override;
void OnWidgetVisibilityChanged(views::Widget* widget, bool visible) override;
void OnWidgetShowStateChanged(views::Widget* widget) override;
void OnWidgetUserResizeEnded(views::Widget* widget) override;
void OnWidgetDestroyed(views::Widget* widget) override;
int FrameViewNonClientHitTest(const gfx::Point& point);
bool IsInAppShim() const override;
void OnCloseButtonClicked();
void CloseNow();
// The remote_cocoa interface of the NSWindow in the shim's process; null
// once it is gone.
remote_cocoa::mojom::NativeWidgetNSWindow* GetNSWindowMojo() const;
void PerformTabAction(mojom::AppShimTabAction action);
// The height the window server adds above the content for this window's
// style: its title bar's, or 0 without one.
int TitleBarHeight() const;
std::unique_ptr<RootViewMac> root_view_;
// How many of the widget's notifications are on the stack. Their listeners
// run JavaScript, and destroying the widget from inside one would free the
// object that is notifying.
// The shim whose process the NSWindow is in.
cppgc::WeakPersistent<api::AppShimHost> app_shim_;
// What the window buttons and tabs were last told, which is what the
// getters answer with: the NSWindow is not here to ask.
std::optional<bool> window_button_visibility_;
std::optional<gfx::Point> traffic_light_position_;
std::optional<std::string> tabbing_identifier_;
// An NSVisualEffectState.
int visual_effect_state_ = 0;
int widget_callback_depth_ = 0;
bool widget_destroyed_ = false;
NSUInteger style_mask_ = 0;
gfx::Rect last_bounds_;
bool was_minimized_ = false;
bool was_maximized_ = false;
bool was_fullscreen_ = false;
bool movable_ = true;
bool closable_ = true;
bool fullscreenable_ = true;
bool is_kiosk_ = false;
bool has_shadow_ = true;
bool content_protected_ = false;
bool excluded_from_windows_menu_ = false;
double opacity_ = 1.0;
SkColor background_color_ = SK_ColorWHITE;
std::string always_on_top_level_;
base::ScopedObservation<views::Widget, views::WidgetObserver>
widget_observation_{this};
base::WeakPtrFactory<NativeWindowMacRemote> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_NATIVE_WINDOW_MAC_REMOTE_H_
File diff suppressed because it is too large. Load diff
+2 -1
View File
@@ -96,7 +96,8 @@ v8::Local<v8::Promise> ShowCertificateTrust(
SecTrustRef trust = nullptr;
SecTrustCreateWithCertificates(cert_chain.get(), sec_policy, &trust);
NSWindow* window = parent_window
// A window in an app shim has no NSWindow here that could take the sheet.
NSWindow* window = parent_window && !parent_window->IsInAppShim()
? parent_window->GetNativeWindow().GetNativeNSWindow()
: nil;
auto msg = base::SysUTF8ToNSString(message);
+22
View File
@@ -11,7 +11,10 @@
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "build/build_config.h"
#include "ui/gfx/native_ui_types.h"
#if BUILDFLAG(IS_LINUX)
#include <bits/stdint-uintn.h>
@@ -83,6 +86,25 @@ std::optional<base::FilePath> ShowSaveDialogSync(
void ShowSaveDialog(const DialogSettings& settings,
gin_helper::Promise<gin_helper::Dictionary> promise);
#if BUILDFLAG(IS_MAC)
using OpenDialogCallback =
base::OnceCallback<void(bool canceled,
const std::vector<base::FilePath>& paths)>;
using SaveDialogCallback =
base::OnceCallback<void(bool canceled, const base::FilePath& path)>;
// The dialogs for a parent that is not a NativeWindow of this process: the
// panel is a sheet of `window`, which must not be null and which the
// settings' own parent is ignored for. An app shim's process shows the
// dialogs of its windows with these.
void ShowOpenDialogForNSWindow(const DialogSettings& settings,
gfx::NativeWindow window,
OpenDialogCallback callback);
void ShowSaveDialogForNSWindow(const DialogSettings& settings,
gfx::NativeWindow window,
SaveDialogCallback callback);
#endif
} // namespace file_dialog
#endif // ELECTRON_SHELL_BROWSER_UI_FILE_DIALOG_H_
+180 -39
View File
@@ -15,11 +15,15 @@
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_cftyperef.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/mac/mac_util.h"
#include "base/run_loop.h"
#include "base/strings/sys_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "content/public/browser/browser_thread.h"
#include "electron/mas.h"
#include "shell/browser/native_window.h"
#include "shell/browser/native_window_mac_remote.h"
#include "shell/common/electron_paths.h"
#include "shell/common/gin_converters/file_path_converter.h"
#include "shell/common/gin_helper/dictionary.h"
@@ -254,16 +258,32 @@ void SetupSaveDialogForProperties(NSSavePanel* dialog, int properties) {
[dialog setTreatsFilePackagesAsDirectories:YES];
}
// The window the dialog is a sheet of, if any. A window in an app shim has no
// NSWindow in this process that could take one.
NSWindow* SheetParent(const DialogSettings& settings) {
if (!settings.parent_window || settings.force_detached ||
settings.parent_window->IsInAppShim()) {
return nil;
}
return settings.parent_window->GetNativeWindow().GetNativeNSWindow();
}
// The window in an app shim that shows the dialog as its sheet, if any.
electron::NativeWindowMacRemote* ShimParent(const DialogSettings& settings) {
if (!settings.parent_window || settings.force_detached ||
!settings.parent_window->IsInAppShim()) {
return nullptr;
}
return static_cast<electron::NativeWindowMacRemote*>(settings.parent_window);
}
// Run modal dialog with parent window and return user's choice.
int RunModalDialog(NSSavePanel* dialog, const DialogSettings& settings) {
__block int chosen = NSModalResponseCancel;
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
settings.force_detached) {
NSWindow* window = SheetParent(settings);
if (!window) {
chosen = [dialog runModal];
} else {
NSWindow* window =
settings.parent_window->GetNativeWindow().GetNativeNSWindow();
[dialog beginSheetModalForWindow:window
completionHandler:^(NSInteger c) {
chosen = c;
@@ -362,11 +382,60 @@ void ResolvePromiseInNextTick(gin_helper::Promise<v8::Local<v8::Value>> promise,
std::move(promise), v8::Global<v8::Value>(promise.isolate(), value)));
}
// Waits, in a nested loop, for a dialog that a shim's process is showing.
template <typename Result>
Result WaitForShimDialog(
base::OnceCallback<void(base::OnceCallback<void(bool, const Result&)>)>
show) {
base::RunLoop run_loop(base::RunLoop::Type::kNestableTasksAllowed);
Result result;
std::move(show).Run(base::BindOnce(
[](base::OnceClosure quit, Result* out, bool canceled,
const Result& value) {
if (!canceled)
*out = value;
std::move(quit).Run();
},
run_loop.QuitClosure(), &result));
run_loop.Run();
return result;
}
gin_helper::Dictionary OpenDialogResult(
v8::Isolate* isolate,
bool canceled,
const std::vector<base::FilePath>& paths,
const std::vector<std::string>& bookmarks) {
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
dict.Set("canceled", canceled);
dict.Set("filePaths", paths);
#if IS_MAS_BUILD()
dict.Set("bookmarks", bookmarks);
#endif
return dict;
}
gin_helper::Dictionary SaveDialogResult(v8::Isolate* isolate,
bool canceled,
const base::FilePath& path) {
auto dict = gin_helper::Dictionary::CreateEmpty(isolate);
dict.Set("canceled", canceled);
dict.Set("filePath", path);
return dict;
}
} // namespace
bool ShowOpenDialogSync(const DialogSettings& settings,
std::vector<base::FilePath>* paths) {
DCHECK(paths);
if (auto* shim_parent = ShimParent(settings)) {
*paths = WaitForShimDialog<std::vector<base::FilePath>>(
base::BindOnce(&electron::NativeWindowMacRemote::ShowOpenDialog,
base::Unretained(shim_parent), settings));
return !paths->empty();
}
NSOpenPanel* dialog = [NSOpenPanel openPanel];
SetupDialog(dialog, settings);
@@ -385,35 +454,41 @@ void OpenDialogCompletion(int chosen,
bool security_scoped_bookmarks,
gin_helper::Promise<gin_helper::Dictionary> promise) {
v8::HandleScope scope(promise.isolate());
auto dict = gin_helper::Dictionary::CreateEmpty(promise.isolate());
if (chosen == NSModalResponseCancel) {
dict.Set("canceled", true);
dict.Set("filePaths", std::vector<base::FilePath>());
std::vector<base::FilePath> paths;
std::vector<std::string> bookmarks;
if (chosen != NSModalResponseCancel) {
#if IS_MAS_BUILD()
dict.Set("bookmarks", std::vector<std::string>());
#endif
} else {
std::vector<base::FilePath> paths;
dict.Set("canceled", false);
#if IS_MAS_BUILD()
std::vector<std::string> bookmarks;
if (security_scoped_bookmarks)
ReadDialogPathsWithBookmarks(dialog, &paths, &bookmarks);
else
ReadDialogPaths(dialog, &paths);
dict.Set("filePaths", paths);
dict.Set("bookmarks", bookmarks);
#else
ReadDialogPaths(dialog, &paths);
dict.Set("filePaths", paths);
#endif
}
ResolvePromiseInNextTick(promise.As<v8::Local<v8::Value>>(),
dict.GetHandle());
ResolvePromiseInNextTick(
promise.As<v8::Local<v8::Value>>(),
OpenDialogResult(promise.isolate(), chosen == NSModalResponseCancel,
paths, bookmarks)
.GetHandle());
}
void ShowOpenDialog(const DialogSettings& settings,
gin_helper::Promise<gin_helper::Dictionary> promise) {
if (auto* shim_parent = ShimParent(settings)) {
shim_parent->ShowOpenDialog(
settings,
base::BindOnce(
[](gin_helper::Promise<gin_helper::Dictionary> promise,
bool canceled, const std::vector<base::FilePath>& paths) {
v8::HandleScope scope(promise.isolate());
promise.Resolve(
OpenDialogResult(promise.isolate(), canceled, paths, {}));
},
std::move(promise)));
return;
}
NSOpenPanel* dialog = [NSOpenPanel openPanel];
SetupDialog(dialog, settings);
@@ -425,26 +500,57 @@ void ShowOpenDialog(const DialogSettings& settings,
__block gin_helper::Promise<gin_helper::Dictionary> p = std::move(promise);
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
settings.force_detached) {
[dialog beginWithCompletionHandler:^(NSInteger chosen) {
OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
std::move(p));
}];
} else {
NSWindow* window =
settings.parent_window->GetNativeWindow().GetNativeNSWindow();
if (NSWindow* window = SheetParent(settings)) {
[dialog
beginSheetModalForWindow:window
completionHandler:^(NSInteger chosen) {
OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
std::move(p));
}];
} else {
[dialog beginWithCompletionHandler:^(NSInteger chosen) {
OpenDialogCompletion(chosen, dialog, security_scoped_bookmarks,
std::move(p));
}];
}
}
void ShowOpenDialogForNSWindow(const DialogSettings& settings,
gfx::NativeWindow window,
OpenDialogCallback callback) {
NSOpenPanel* dialog = [NSOpenPanel openPanel];
SetupDialog(dialog, settings);
SetupOpenDialogForProperties(dialog, settings.properties);
__block OpenDialogCallback cb = std::move(callback);
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunner::GetCurrentDefault();
[dialog
beginSheetModalForWindow:window.GetNativeNSWindow()
completionHandler:^(NSInteger chosen) {
std::vector<base::FilePath> paths;
if (chosen != NSModalResponseCancel)
ReadDialogPaths(dialog, &paths);
// Not from inside the panel's own transaction.
task_runner->PostTask(
FROM_HERE, base::BindOnce(std::move(cb),
chosen == NSModalResponseCancel,
std::move(paths)));
}];
}
std::optional<base::FilePath> ShowSaveDialogSync(
const DialogSettings& settings) {
if (auto* shim_parent = ShimParent(settings)) {
base::FilePath path = WaitForShimDialog<base::FilePath>(
base::BindOnce(&electron::NativeWindowMacRemote::ShowSaveDialog,
base::Unretained(shim_parent), settings));
if (path.empty())
return {};
return path;
}
NSSavePanel* dialog = [NSSavePanel savePanel];
SetupDialog(dialog, settings);
@@ -487,6 +593,19 @@ void SaveDialogCompletion(int chosen,
void ShowSaveDialog(const DialogSettings& settings,
gin_helper::Promise<gin_helper::Dictionary> promise) {
if (auto* shim_parent = ShimParent(settings)) {
shim_parent->ShowSaveDialog(
settings, base::BindOnce(
[](gin_helper::Promise<gin_helper::Dictionary> promise,
bool canceled, const base::FilePath& path) {
v8::HandleScope scope(promise.isolate());
promise.Resolve(SaveDialogResult(promise.isolate(),
canceled, path));
},
std::move(promise)));
return;
}
NSSavePanel* dialog = [NSSavePanel savePanel];
SetupDialog(dialog, settings);
@@ -499,22 +618,44 @@ void ShowSaveDialog(const DialogSettings& settings,
__block gin_helper::Promise<gin_helper::Dictionary> p = std::move(promise);
if (!settings.parent_window || !settings.parent_window->GetNativeWindow() ||
settings.force_detached) {
[dialog beginWithCompletionHandler:^(NSInteger chosen) {
SaveDialogCompletion(chosen, dialog, security_scoped_bookmarks,
std::move(p));
}];
} else {
NSWindow* window =
settings.parent_window->GetNativeWindow().GetNativeNSWindow();
if (NSWindow* window = SheetParent(settings)) {
[dialog
beginSheetModalForWindow:window
completionHandler:^(NSInteger chosen) {
SaveDialogCompletion(chosen, dialog, security_scoped_bookmarks,
std::move(p));
}];
} else {
[dialog beginWithCompletionHandler:^(NSInteger chosen) {
SaveDialogCompletion(chosen, dialog, security_scoped_bookmarks,
std::move(p));
}];
}
}
void ShowSaveDialogForNSWindow(const DialogSettings& settings,
gfx::NativeWindow window,
SaveDialogCallback callback) {
NSSavePanel* dialog = [NSSavePanel savePanel];
SetupDialog(dialog, settings);
SetupSaveDialogForProperties(dialog, settings.properties);
[dialog setCanSelectHiddenExtension:YES];
__block SaveDialogCallback cb = std::move(callback);
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunner::GetCurrentDefault();
[dialog
beginSheetModalForWindow:window.GetNativeNSWindow()
completionHandler:^(NSInteger chosen) {
base::FilePath path;
if (chosen != NSModalResponseCancel && [[dialog URL] isFileURL])
path = base::FilePath(
base::SysNSStringToUTF8([[dialog URL] path]));
task_runner->PostTask(FROM_HERE,
base::BindOnce(std::move(cb), path.empty(),
std::move(path)));
}];
}
} // namespace file_dialog
+14
View File
@@ -11,7 +11,9 @@
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr_exclusion.h"
#include "build/build_config.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/native_ui_types.h"
namespace electron {
@@ -56,6 +58,18 @@ void ShowMessageBox(const MessageBoxSettings& settings,
void CloseMessageBox(int id);
#if BUILDFLAG(IS_MAC)
// CloseMessageBox(), if a box with this id is showing; nothing otherwise.
void CloseMessageBoxIfShowing(int id);
// ShowMessageBox() for a parent that is not a NativeWindow of this process:
// the box is a sheet of `window`, or stands alone when `window` is null. An
// app shim's process shows the boxes of its windows with it.
void ShowMessageBoxForNSWindow(const MessageBoxSettings& settings,
gfx::NativeWindow window,
MessageBoxCallback callback);
#endif
// Like ShowMessageBox with simplest settings, but safe to call at very early
// stage of application.
void ShowErrorBox(const std::u16string& title, const std::u16string& content);
+40 -12
View File
@@ -13,8 +13,9 @@
#include "base/mac/mac_util.h"
#include "base/no_destructor.h"
#include "base/strings/sys_string_conversions.h"
#include "content/public/browser/browser_thread.h"
#include "base/task/sequenced_task_runner.h"
#include "shell/browser/native_window.h"
#include "shell/browser/native_window_mac_remote.h"
#include "skia/ext/skia_utils_mac.h"
#include "ui/gfx/image/image_skia.h"
@@ -110,6 +111,11 @@ NSAlert* CreateNSAlert(const MessageBoxSettings& settings) {
} // namespace
int ShowMessageBoxSync(const MessageBoxSettings& settings) {
if (settings.parent_window && settings.parent_window->IsInAppShim()) {
return static_cast<NativeWindowMacRemote*>(settings.parent_window)
->ShowMessageBoxSync(settings);
}
NSAlert* alert(CreateNSAlert(settings));
// Use runModal for synchronous alert without parent, since we don't have a
@@ -135,31 +141,43 @@ int ShowMessageBoxSync(const MessageBoxSettings& settings) {
void ShowMessageBox(const MessageBoxSettings& settings,
MessageBoxCallback callback) {
if (settings.parent_window && settings.parent_window->IsInAppShim()) {
static_cast<NativeWindowMacRemote*>(settings.parent_window)
->ShowMessageBox(settings, std::move(callback));
return;
}
ShowMessageBoxForNSWindow(settings,
settings.parent_window
? settings.parent_window->GetNativeWindow()
: gfx::NativeWindow(),
std::move(callback));
}
void ShowMessageBoxForNSWindow(const MessageBoxSettings& settings,
gfx::NativeWindow native_window,
MessageBoxCallback callback) {
NSAlert* alert = CreateNSAlert(settings);
NSWindow* window = native_window.GetNativeNSWindow();
// Use runModal for synchronous alert without parent, since we don't have a
// window to wait for.
if (!settings.parent_window) {
if (!window) {
int ret = [alert runModal];
std::move(callback).Run(
ret, alert.suppressionButton.state == NSControlStateValueOn);
} else {
if (settings.id) {
if (GetDialogsMap().contains(*settings.id))
CloseMessageBox(*settings.id);
CloseMessageBoxIfShowing(*settings.id);
GetDialogsMap()[*settings.id] = alert;
}
NSWindow* window =
settings.parent_window
? settings.parent_window->GetNativeWindow().GetNativeNSWindow()
: nil;
// Duplicate the callback object here since c is a reference and gcd would
// only store the pointer, by duplication we can force gcd to store a copy.
__block MessageBoxCallback callback_ = std::move(callback);
__block std::optional<int> id = std::move(settings.id);
__block int cancel_id = settings.cancel_id;
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::SequencedTaskRunner::GetCurrentDefault();
auto handler = ^(NSModalResponse response) {
if (id)
@@ -175,16 +193,26 @@ void ShowMessageBox(const MessageBoxSettings& settings,
// users will run in the callback, we have to delay running the callback
// until next tick, otherwise crash like this may happen:
// https://github.com/electron/electron/issues/26884
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(std::move(callback_), response, suppressed));
task_runner->PostTask(FROM_HERE, base::BindOnce(std::move(callback_),
response, suppressed));
};
[alert beginSheetModalForWindow:window completionHandler:handler];
}
}
void CloseMessageBoxIfShowing(int id) {
if (auto it = GetDialogsMap().find(id); it != GetDialogsMap().end()) {
[NSApp endSheet:it->second.window];
return;
}
// One that a window in an app shim is showing, in the shim's process.
NativeWindowMacRemote::CloseMessageBox(id);
}
void CloseMessageBox(int id) {
auto it = GetDialogsMap().find(id);
if (it == GetDialogsMap().end() && NativeWindowMacRemote::CloseMessageBox(id))
return;
if (it == GetDialogsMap().end()) {
LOG(ERROR) << "CloseMessageBox called with nonexistent ID";
return;
@@ -0,0 +1,33 @@
// Copyright (c) 2013 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/ui/views/content_view_targeter_delegate.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/views/view.h"
namespace electron {
bool ContentViewTargeterDelegate::DoesIntersectRect(
const views::View* target,
const gfx::Rect& rect) const {
if (!views::ViewTargeterDelegate::DoesIntersectRect(target, rect)) {
return false;
}
for (const views::View* child : target->children()) {
if (!child->GetVisible() || !child->GetCanProcessEventsWithinSubtree()) {
continue;
}
gfx::RectF rect_in_child(rect);
views::View::ConvertRectToTarget(target, child, &rect_in_child);
if (child->HitTestRect(gfx::ToEnclosingRect(rect_in_child))) {
return true;
}
}
return false;
}
} // namespace electron
@@ -0,0 +1,46 @@
// Copyright (c) 2013 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_BROWSER_UI_VIEWS_CONTENT_VIEW_TARGETER_DELEGATE_H_
#define ELECTRON_SHELL_BROWSER_UI_VIEWS_CONTENT_VIEW_TARGETER_DELEGATE_H_
#include "ui/views/view_targeter_delegate.h"
namespace gfx {
class Rect;
} // namespace gfx
namespace views {
class View;
} // namespace views
namespace electron {
// A ViewTargeterDelegate for a window's content view that makes it
// hit-test-transparent except where one of its descendants covers the
// target rect.
//
// In BrowserWindow, the WebContentsView is added as a sibling of
// content_view_ at a lower z-order so that user-added child views paint
// above web content. content_view_ itself covers the full window, so the
// default DoesIntersectRect (a bounds check) would always return true and
// the parent targeter would stop there, never reaching the WebContentsView
// behind it. By only intersecting where a child actually does, the parent's
// default TargetForRect loop skips content_view_ and falls through to the
// sibling, ultimately resolving to the WebContentsView's NativeViewHost so
// BridgedContentView::hitTest: routes the event to RenderWidgetHostViewCocoa.
//
// This must never affect TargetForRect's contract — it only narrows
// HitTestRect, which the default loop already handles by skipping the child.
// Returning nullptr from TargetForRect (the previous approach) violated the
// contract and crashed RootView::UpdateCursor (see #51576).
class ContentViewTargeterDelegate : public views::ViewTargeterDelegate {
public:
bool DoesIntersectRect(const views::View* target,
const gfx::Rect& rect) const override;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_UI_VIEWS_CONTENT_VIEW_TARGETER_DELEGATE_H_
+14
View File
@@ -85,3 +85,17 @@ mojom("web_contents_utility") {
"//url/mojom:url_mojom_origin",
]
}
if (is_mac) {
mojom("app_shim") {
sources = [ "app_shim.mojom" ]
public_deps = [
"//components/remote_cocoa/common:mojo",
"//mojo/public/mojom/base",
"//ui/gfx/geometry/mojom",
"//ui/gfx/image/mojom",
"//url/mojom:url_mojom_gurl",
]
}
}
+205
View File
@@ -0,0 +1,205 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
module electron.mojom;
import "components/remote_cocoa/common/application.mojom";
import "mojo/public/mojom/base/file_path.mojom";
import "mojo/public/mojom/base/string16.mojom";
import "ui/gfx/geometry/mojom/geometry.mojom";
import "ui/gfx/image/mojom/image.mojom";
import "url/mojom/url.mojom";
enum AppShimAttentionType {
kCancel,
kInformational,
kCritical,
};
// How much of the accessibility tree an assistive technology that is looking
// at the shim needs. kPartial is the native controls; kComplete adds web
// content, which a screen reader needs.
enum AppShimAccessibilityMode {
kPartial,
kComplete,
};
// electron::MessageBoxSettings, less the parent window.
struct AppShimMessageBoxOptions {
// An electron::MessageBoxType.
int32 type;
array<string> buttons;
int32 default_id;
int32 cancel_id;
string message;
string detail;
string checkbox_label;
bool checkbox_checked;
gfx.mojom.ImageSkia? icon;
int32 text_width;
// What AppShim.CloseMessageBox names the box by, when it has one.
bool has_id;
int32 id;
};
struct AppShimFileDialogFilter {
string name;
array<string> extensions;
};
// file_dialog::DialogSettings, less the parent window.
struct AppShimFileDialogOptions {
string title;
string message;
string button_label;
string name_field_label;
mojo_base.mojom.FilePath default_path;
array<AppShimFileDialogFilter> filters;
// file_dialog::OpenFileDialogProperty or SaveFileDialogProperty bits.
int32 properties;
bool shows_tag_field;
};
// What a window was created with that remote_cocoa has no call for. Sent
// once, right after the window is made.
struct AppShimWindowChrome {
bool has_frame;
// An electron::NativeWindow::TitleBarStyle.
int32 title_bar_style;
gfx.mojom.Point? traffic_light_position;
int32 titlebar_overlay_height;
// Null for a window that takes no part in tabbing.
string? tabbing_identifier;
};
enum AppShimTabAction {
kSelectPrevious,
kSelectNext,
kShowAll,
kMergeAll,
kMoveToNewWindow,
kToggleTabBar,
};
// One item of a menu, as an ElectronMenuModel describes it.
struct AppShimMenuItem {
// A ui::MenuModel::ItemType.
int32 type;
int32 command_id;
mojo_base.mojom.String16 label;
mojo_base.mojom.String16 secondary_label;
mojo_base.mojom.String16 tool_tip;
mojo_base.mojom.String16 accessibility_label;
mojo_base.mojom.String16 role;
mojo_base.mojom.String16 custom_type;
gfx.mojom.ImageSkia? icon;
// A ui::Accelerator: a ui::KeyboardCode, ui::EventFlags, and the shifted
// character if the accelerator names one (0 if not).
bool has_accelerator;
int32 accelerator_key_code;
int32 accelerator_modifiers;
uint16 accelerator_shifted_char;
bool register_accelerator;
bool works_when_hidden;
bool checked;
bool enabled;
bool visible;
int32 group_id;
array<AppShimMenuItem>? submenu;
};
// Implemented by the app shim process and called by the browser process.
interface AppShim {
// Binds the remote_cocoa application through which the browser process
// creates the NSWindows and NSViews that this shim hosts.
CreateRemoteCocoaApplication(
pending_associated_receiver<remote_cocoa.mojom.Application> application);
// Sets the label on the shim's Dock tile. An empty label clears it.
SetBadgeLabel(string label);
// Bounces, or stops bouncing, the shim's Dock tile.
SetUserAttention(AppShimAttentionType type);
// Terminates the shim process.
Quit();
// Answers AppShimHost.QuitRequested with "keep running".
CancelQuit();
// The menu bar to show while the shim is the active app, which is the
// shim's own and not the browser's. The browser sends it again whenever an
// item's state may have changed. Null puts back the short menu that the
// shim builds for itself.
SetApplicationMenu(array<AppShimMenuItem>? items);
// The parts of the window API that act on the NSWindow itself and that
// remote_cocoa has no call for. `window_id` is the remote_cocoa id of the
// window; a call for a window that is gone does nothing.
InitWindowChrome(uint64 window_id, AppShimWindowChrome chrome);
SetWindowButtonVisibility(uint64 window_id, bool visible);
SetWindowButtonPosition(uint64 window_id, gfx.mojom.Point? position);
// An empty `type` removes the effect. `state` is an
// NSVisualEffectState.
SetWindowVibrancy(
uint64 window_id, string type, int32 duration_ms, int32 state);
PerformWindowTabAction(uint64 window_id, AppShimTabAction action);
AddTabbedWindow(uint64 window_id, uint64 tabbed_window_id);
PreviewFile(
uint64 window_id, mojo_base.mojom.FilePath path, string display_name);
CloseFilePreview(uint64 window_id);
// Dialogs that are sheets of one of the shim's windows, which only the
// shim's process can show. `window_id` is the remote_cocoa id of the
// window; the dialog is cancelled when no such window is left.
ShowMessageBox(uint64 window_id, AppShimMessageBoxOptions options)
=> (int32 response, bool checkbox_checked);
CloseMessageBox(int32 id);
ShowOpenDialog(uint64 window_id, AppShimFileDialogOptions options)
=> (bool canceled, array<mojo_base.mojom.FilePath> paths);
ShowSaveDialog(uint64 window_id, AppShimFileDialogOptions options)
=> (bool canceled, mojo_base.mojom.FilePath path);
};
// Implemented by the browser process and called by the app shim process.
interface AppShimHost {
// The shim's Dock tile was clicked, or it was opened again while running.
ReopenApp(bool has_visible_windows);
// The shim was asked to open files or URLs.
FilesOpened(array<mojo_base.mojom.FilePath> files);
UrlsOpened(array<url.mojom.Url> urls);
// An assistive technology started looking at the shim. The accessibility
// tree of its windows is built in the browser process, which must turn it
// on.
EnableAccessibilitySupport(AppShimAccessibilityMode mode);
// An item of the menu from AppShim.SetApplicationMenu was chosen. `path`
// is the index of the item in each menu on the way to it, from the menu
// bar down; `command_id` is the item's, so that the browser can tell when
// its menu has changed since.
MenuItemActivated(array<uint32> path, int32 command_id, int32 event_flags);
// A menu of the menu bar is about to open, or has closed.
MenuWillShow();
MenuWillClose();
// The "+" of a window's tab bar was clicked.
NewWindowForTab(uint64 window_id);
// The shim became the active app, which is a good moment for the browser
// to send the menu again.
DidBecomeActive();
// The person, or the system at logout, asked the shim to quit. The browser
// answers with AppShim.Quit or AppShim.CancelQuit; the shim quits by
// itself if neither comes soon.
QuitRequested();
};
// The first interface served on the connection a shim makes to the browser.
interface AppShimHostBootstrap {
// `app_shim_receiver` is null when the browser turns the shim away.
OnShimConnected(pending_receiver<AppShimHost> host_receiver)
=> (pending_receiver<AppShim>? app_shim_receiver);
};
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/common/app_shim_constants.h"
#include "base/containers/span.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/common/chrome_version.h"
#include "crypto/hash.h"
#include "electron/electron_version.h"
namespace electron::app_shim {
std::string BuildMarkerServerName(const std::string& server_name) {
// Mojo message IDs are scrambled per Chromium version, and Electron's own
// interface with the shim can change with every Electron version.
const auto build =
crypto::hash::Sha256(ELECTRON_VERSION_STRING "/" CHROME_VERSION_STRING);
return base::StrCat(
{server_name, ".", base::HexEncode(base::span(build).first<8>())});
}
} // namespace electron::app_shim
+54
View File
@@ -0,0 +1,54 @@
// Copyright (c) 2026 Anthropic, PBC.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_APP_SHIM_CONSTANTS_H_
#define ELECTRON_SHELL_COMMON_APP_SHIM_CONSTANTS_H_
#include <mach/message.h>
#include <string>
namespace electron::app_shim {
// The --type of an app shim process. It must be the type that the sandbox
// policy lists as an unsandboxed process (sandbox::policy::switches::
// kAppShimProcessType), or content refuses to start it.
inline constexpr char kProcessType[] = "app-shim";
// Mach message ID a shim sends to the browser's bootstrap server to connect.
inline constexpr mach_msg_id_t kBootstrapMsgId = 'eshm';
// Mach message ID a shim sends to that server, in place of connecting, when
// it finds that the browser is another build of Electron than the one it
// loaded. The message has the layout of the one above and no channel.
inline constexpr mach_msg_id_t kBuildMismatchMsgId = 'eshv';
// The name of a second bootstrap server that the browser publishes beside
// `server_name` and never reads from. It is there for a shim to look up: the
// shim loaded the Electron Framework that is on disk, the browser is running
// the one that was on disk when it started, and when the app has been
// replaced in between, the two can't talk Mojo to each other. The name holds
// the build of the framework, so a shim finds it only when the builds match.
//
// TODO(MarshallOfSound): keep each version's framework in a folder of its own
// (Versions/<version>, as Chrome does) and have the shim load the one that
// the running browser uses. A mismatch can't happen then, and this goes away.
std::string BuildMarkerServerName(const std::string& server_name);
// The browser publishes its bootstrap server as
// "<base bundle id>.<name>". This is the default name.
inline constexpr char kDefaultServerName[] = "appshim";
// Keys in the shim bundle's Info.plist.
// The name the browser passed to app.startAppShimServer(), when not the
// default.
inline constexpr char kInfoPlistServerNameKey[] = "ElectronAppShimServerName";
// Arguments to start the host app with when it is not running. An array of
// strings.
inline constexpr char kInfoPlistHostArgumentsKey[] =
"ElectronAppShimHostArguments";
} // namespace electron::app_shim
#endif // ELECTRON_SHELL_COMMON_APP_SHIM_CONSTANTS_H_
@@ -13,6 +13,7 @@ namespace electron {
// Electron-specific WrappablePointerTag values that extend gin's tag range.
enum ElectronWrappablePointerTag : uint16_t {
kElectronApp = gin::kLastPointerTag + 1, // electron::api::App
kElectronAppShimHost, // electron::api::AppShimHost
kElectronAutoUpdater, // electron::api::AutoUpdater
kElectronChunkedDataPipeReadableStream, // ChunkedDataPipeReadableStream
kElectronClipboardItem, // electron::api::ClipboardItem
+1
View File
@@ -14,6 +14,7 @@ namespace electron {
namespace options {
inline constexpr std::string_view kTitle = "title";
inline constexpr std::string_view kAppShim = "appShim";
inline constexpr std::string_view kIcon = "icon";
inline constexpr std::string_view kFrame = "frame";
inline constexpr std::string_view kShow = "show";
+380
View File
@@ -0,0 +1,380 @@
import { app, BaseWindow, BrowserWindow, dialog, Menu } from 'electron/main';
import { afterAll, afterEach, beforeAll, expect, it } from 'vitest';
import * as cp from 'node:child_process';
import { once } from 'node:events';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { setTimeout } from 'node:timers/promises';
import { ifdescribe, waitUntil } from './lib/spec-helpers.ts';
import { closeAllWindows } from './lib/window-helpers.ts';
// Electron.app, which the specs run in and which hosts the shims.
const hostAppPath = path.resolve(process.execPath, '..', '..', '..');
const loaderPath = path.join(
hostAppPath,
'Contents',
'Frameworks',
'Electron Framework.framework',
'Helpers',
'app_shim_loader'
);
const SHIM_NAME = 'Spec Shim';
const SHIM_BUNDLE_ID = 'org.electronjs.spec.app-shim';
type OnScreenWindow = { pid: number; x: number; y: number; width: number; height: number };
// The windows the window server has on screen, with the process that owns
// each. Read with JXA so that the answer comes from outside Electron.
function onScreenWindows(): OnScreenWindow[] {
const script = `
ObjC.import('CoreGraphics');
const list = ObjC.deepUnwrap(ObjC.castRefToObject(
$.CGWindowListCopyWindowInfo($.kCGWindowListOptionOnScreenOnly, 0)));
JSON.stringify(list.map((w) => ({
pid: w.kCGWindowOwnerPID,
x: w.kCGWindowBounds.X,
y: w.kCGWindowBounds.Y,
width: w.kCGWindowBounds.Width,
height: w.kCGWindowBounds.Height
})));`;
const { stdout } = cp.spawnSync('osascript', ['-l', 'JavaScript', '-e', script], { encoding: 'utf8' });
return JSON.parse(stdout);
}
ifdescribe(process.platform === 'darwin' && !process.mas)('app shims', () => {
// Another Electron running specs must not get this one's shims.
const serverName = `spec-${process.pid}`;
let tmpDir: string;
let bundlePath: string;
let shimProcess: cp.ChildProcess | null = null;
beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-app-shim-'));
bundlePath = path.join(tmpDir, `${SHIM_NAME}.app`);
const macOSDir = path.join(bundlePath, 'Contents', 'MacOS');
fs.mkdirSync(macOSDir, { recursive: true });
fs.copyFileSync(loaderPath, path.join(macOSDir, SHIM_NAME));
fs.writeFileSync(
path.join(bundlePath, 'Contents', 'Info.plist'),
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key><string>${SHIM_NAME}</string>
<key>CFBundleIdentifier</key><string>${SHIM_BUNDLE_ID}</string>
<key>CFBundleName</key><string>${SHIM_NAME}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>NSHighResolutionCapable</key><true/>
<key>ElectronAppShimHostPath</key><string>${hostAppPath}</string>
<key>ElectronAppShimServerName</key><string>${serverName}</string>
</dict>
</plist>
`
);
app.startAppShimServer({ name: serverName });
});
afterAll(() => {
app.stopAppShimServer();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
afterEach(async () => {
await closeAllWindows();
if (shimProcess && shimProcess.exitCode === null && shimProcess.signalCode === null) {
const exited = once(shimProcess, 'exit');
shimProcess.kill('SIGKILL');
await exited;
}
shimProcess = null;
});
async function launchShim(): Promise<Electron.AppShimHost> {
const connected = once(app, 'app-shim-connected');
shimProcess = cp.spawn(path.join(bundlePath, 'Contents', 'MacOS', SHIM_NAME), { stdio: 'inherit' });
const [, shim] = await connected;
return shim;
}
it('throws when the server is started twice', () => {
expect(() => app.startAppShimServer({ name: serverName })).toThrow(/already started/);
});
it('connects a shim and says which process it is', async () => {
const shim = await launchShim();
expect(shim.isConnected()).toBe(true);
expect(shim.pid).toBe(shimProcess!.pid);
expect(shim.bundleId).toBe(SHIM_BUNDLE_ID);
expect(fs.realpathSync(shim.bundlePath)).toBe(fs.realpathSync(bundlePath));
});
it('checks the shim against a code requirement', async () => {
const shim = await launchShim();
expect(shim.satisfiesCodeRequirement(`identifier "${SHIM_BUNDLE_ID}.not-this"`)).toBe(false);
expect(() => shim.satisfiesCodeRequirement('not a requirement')).toThrow(/Invalid code requirement/);
});
it('creates the window in the shim process', async () => {
const shim = await launchShim();
const size = { width: 432, height: 321 };
const w = new BrowserWindow({ appShim: shim, ...size });
await w.loadURL('data:text/html,<title>in a shim</title><body style="background:teal">');
expect(await w.webContents.executeJavaScript('document.title')).toBe('in a shim');
const sameSize = (win: OnScreenWindow) => win.width === size.width && win.height === size.height;
await waitUntil(() => onScreenWindows().some((win) => win.pid === shim.pid && sameSize(win)));
// Its stand-in in this process is never on screen.
expect(onScreenWindows().some((win) => win.pid === process.pid && sameSize(win))).toBe(false);
});
it('reports the bounds and state of a window in a shim', async () => {
const shim = await launchShim();
const w = new BrowserWindow({ appShim: shim, x: 80, y: 90, width: 400, height: 300 });
// The shim's process shows the window, and says so afterwards.
await once(w, 'show');
expect(w.isVisible()).toBe(true);
expect(w.getBounds()).toEqual({ x: 80, y: 90, width: 400, height: 300 });
w.setSize(500, 350);
expect(w.getSize()).toEqual([500, 350]);
// What the shim's process did with it, according to the window server.
await waitUntil(() =>
onScreenWindows().some((win) => win.pid === shim.pid && win.width === 500 && win.height === 350)
);
const hidden = once(w, 'hide');
w.hide();
await hidden;
expect(w.isVisible()).toBe(false);
});
it('lets the close event keep a window in a shim open', async () => {
const shim = await launchShim();
// A BaseWindow closes inside close() when nothing prevents it.
const w = new BaseWindow({ appShim: shim });
let closedEmitted = false;
w.once('closed', () => {
closedEmitted = true;
});
w.once('close', (event) => event.preventDefault());
w.close();
await setTimeout(100);
expect(closedEmitted).toBe(false);
expect(w.isDestroyed()).toBe(false);
w.close();
expect(closedEmitted).toBe(true);
expect(shim.isConnected()).toBe(true);
});
it('sizes a framed window by its content when asked to', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim, width: 400, height: 300, useContentSize: true });
expect(w.getContentSize()).toEqual([400, 300]);
const titleBarHeight = w.getSize()[1] - 300;
expect(titleBarHeight).toBeGreaterThan(0);
// The limits are the window's, title bar included.
w.setMinimumSize(300, 200);
w.setSize(100, 100);
await waitUntil(() =>
onScreenWindows().some((win) => win.pid === shim.pid && win.width === 300 && win.height === 200)
);
});
it('survives a window being destroyed by its own show listener', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim });
const closed = once(w, 'closed');
w.once('show', () => w.destroy());
await closed;
expect(w.isDestroyed()).toBe(true);
expect(shim.isConnected()).toBe(true);
});
it('survives the shim being quit by a show listener of its window', async () => {
const shim = await launchShim();
const exited = once(shimProcess!, 'exit');
const w = new BaseWindow({ appShim: shim });
const closed = once(w, 'closed');
// What the listener saw, checked below: a failed expectation inside a
// listener is an uncaught exception, not a failed test.
let connectedAfterQuit: boolean | undefined;
let errorFromNewWindow: unknown;
w.once('show', () => {
shim.quit();
connectedAfterQuit = shim.isConnected();
try {
const unexpected = new BaseWindow({ appShim: shim });
unexpected.destroy();
} catch (error) {
errorFromNewWindow = error;
}
});
await Promise.all([closed, exited, once(shim, 'disconnected')]);
expect(connectedAfterQuit).toBe(false);
expect(errorFromNewWindow).toBeInstanceOf(Error);
expect((errorFromNewWindow as Error).message).toMatch(/disconnected/);
});
it('restores from the Dock without leaving the maximized state', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim, width: 400, height: 300 });
await once(w, 'show');
const maximized = once(w, 'maximize');
w.maximize();
await maximized;
const minimized = once(w, 'minimize');
w.minimize();
await minimized;
const restored = once(w, 'restore');
w.restore();
await restored;
expect(w.isMaximized()).toBe(true);
});
it('does not announce a shim after the server is stopped', async () => {
app.stopAppShimServer();
try {
let announced = false;
const onConnected = () => {
announced = true;
};
app.once('app-shim-connected', onConnected);
shimProcess = cp.spawn(path.join(bundlePath, 'Contents', 'MacOS', SHIM_NAME), { stdio: 'inherit' });
await setTimeout(1500);
app.removeListener('app-shim-connected', onConnected);
expect(announced).toBe(false);
} finally {
app.startAppShimServer({ name: serverName });
}
});
it('rejects options that are not what startAppShimServer takes', () => {
app.stopAppShimServer();
try {
expect(() => app.startAppShimServer({ name: 123 as any })).toThrow(/name must be a string/);
expect(() => app.startAppShimServer({ name: 'not valid!' })).toThrow(/may only hold/);
} finally {
app.startAppShimServer({ name: serverName });
}
});
it('shows a message box as a sheet in the shim process', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim, width: 400, height: 300 });
await once(w, 'show');
const windowsOfShim = () => onScreenWindows().filter((win) => win.pid === shim.pid).length;
await waitUntil(() => windowsOfShim() === 1);
const controller = new AbortController();
const result = dialog.showMessageBox(w, {
message: 'A sheet',
buttons: ['OK', 'Cancel'],
cancelId: 1,
signal: controller.signal
});
// The sheet is a second window of the shim's process, not of this one.
await waitUntil(() => windowsOfShim() === 2);
controller.abort();
expect((await result).response).toBe(1);
await waitUntil(() => windowsOfShim() === 1);
});
it('answers a dialog with "cancelled" when its shim goes away', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim, width: 400, height: 300 });
await once(w, 'show');
const result = dialog.showOpenDialog(w, { properties: ['openFile'] });
await waitUntil(() => onScreenWindows().filter((win) => win.pid === shim.pid).length >= 2);
shimProcess!.kill('SIGKILL');
expect(await result).toEqual({ canceled: true, filePaths: [] });
});
it('takes a menu of its own, or null, and nothing else', async () => {
const shim = await launchShim();
const menu = Menu.buildFromTemplate([{ role: 'appMenu' }, { label: 'Tools', submenu: [{ label: 'Ping' }] }]);
expect(() => shim.setApplicationMenu(menu)).not.toThrow();
expect(() => shim.setApplicationMenu(null)).not.toThrow();
expect(() => shim.setApplicationMenu('menu' as any)).toThrow(/Must pass a Menu or null/);
expect(() => (shim as any).setApplicationMenu()).toThrow(/Must pass a Menu or null/);
expect(shim.isConnected()).toBe(true);
});
it('gives a window with a hidden title bar its whole frame as content', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim, width: 400, height: 300, titleBarStyle: 'hidden' });
await once(w, 'show');
expect(w.getContentSize()).toEqual([400, 300]);
await waitUntil(() =>
onScreenWindows().some((win) => win.pid === shim.pid && win.width === 400 && win.height === 300)
);
// The window buttons are the shim's to move and hide.
w.setWindowButtonPosition({ x: 20, y: 20 });
expect(w.getWindowButtonPosition()).toEqual({ x: 20, y: 20 });
w.setWindowButtonVisibility(false);
w.setVibrancy('sidebar');
w.setVibrancy(null);
await setTimeout(200);
expect(shim.isConnected()).toBe(true);
});
it('tabs two windows of one shim together, and no others', async () => {
const shim = await launchShim();
const windowsOfShim = () => onScreenWindows().filter((win) => win.pid === shim.pid).length;
const a = new BaseWindow({ appShim: shim, width: 400, height: 300, tabbingIdentifier: 'group' });
const b = new BaseWindow({ appShim: shim, width: 400, height: 300, tabbingIdentifier: 'group' });
expect(a.tabbingIdentifier).toBe('group');
await waitUntil(() => windowsOfShim() === 2);
a.addTabbedWindow(b);
// One window on screen, with the other as a tab in it.
await waitUntil(() => windowsOfShim() === 1);
const local = new BaseWindow({ show: false, tabbingIdentifier: 'group' });
expect(() => a.addTabbedWindow(local)).toThrow(/same app/);
expect(() => local.addTabbedWindow(a)).toThrow(/same app/);
local.destroy();
});
it('does not take a window in a shim as a parent', async () => {
const shim = await launchShim();
const w = new BaseWindow({ appShim: shim });
expect(() => new BaseWindow({ parent: w })).toThrow(/can't be a parent window/);
const other = new BaseWindow({ show: false });
expect(() => other.setParentWindow(w)).toThrow(/can't be a parent window/);
// Nor does it take one.
expect(() => new BaseWindow({ appShim: shim, parent: other })).toThrow(/can't have a parent window/);
expect(() => w.setParentWindow(other)).toThrow(/can't have a parent window/);
expect(w.getParentWindow()).toBeNull();
other.destroy();
});
it('closes the windows of a shim that goes away', async () => {
const shim = await launchShim();
const w = new BrowserWindow({ appShim: shim });
const closed = once(w, 'closed');
const disconnected = once(shim, 'disconnected');
shimProcess!.kill('SIGKILL');
await Promise.all([closed, disconnected]);
expect(w.isDestroyed()).toBe(true);
expect(shim.isConnected()).toBe(false);
expect(() => new BrowserWindow({ appShim: shim })).toThrow(/disconnected/);
});
it('ends the shim process on quit()', async () => {
const shim = await launchShim();
const exited = once(shimProcess!, 'exit');
shim.quit();
await exited;
expect(shim.isConnected()).toBe(false);
});
it('rejects an appShim that is not an AppShimHost', () => {
expect(() => new BrowserWindow({ appShim: {} as any })).toThrow(/must be an AppShimHost/);
});
});