From 84ff50715b6c56cba70ad4a5caf6bb0f446c416b Mon Sep 17 00:00:00 2001 From: snekxs <26660858+snekxs@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:24:21 -0600 Subject: [PATCH 1/2] macOS: menu bar tray, link fix, macOS CI build; faster ci profile - build.rs: only pass /STACK to MSVC targets (broke linking on macOS) - tray: macOS menu bar icon, shared with the Windows implementation - CI: macOS build workflow producing an ad-hoc-signed .dmg - CI: new 'ci' cargo profile (no LTO) used by PR/main builds --- .github/scripts/make-macos-app.sh | 50 ++++++++++ .github/workflows/build-gpui-macos.yml | 56 +++++++++++ .github/workflows/build-gpui-windows.yml | 12 ++- gpui/Cargo.toml | 9 ++ gpui/build.rs | 4 +- gpui/src/bin/main.rs | 1 - gpui/src/tray.rs | 121 ++++++++++++++++------- 7 files changed, 212 insertions(+), 41 deletions(-) create mode 100755 .github/scripts/make-macos-app.sh create mode 100644 .github/workflows/build-gpui-macos.yml diff --git a/.github/scripts/make-macos-app.sh b/.github/scripts/make-macos-app.sh new file mode 100755 index 0000000..1354d6a --- /dev/null +++ b/.github/scripts/make-macos-app.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Wraps a built openmouse-gpui binary into OpenMouse.app and a .dmg. +# usage: make-macos-app.sh +set -euo pipefail + +binary=$1 +version=$2 +out=$3 +logo="$(dirname "$0")/../../gpui/assets/logo.png" + +app="$out/OpenMouse.app" +rm -rf "$app" "$out/OpenMouse.dmg" +mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +cp "$binary" "$app/Contents/MacOS/openmouse-gpui" + +# The logo is portrait, so pad it to a square before making the icon set. +work=$(mktemp -d) +sips --padToHeightWidth 268 268 --padColor 000000 "$logo" --out "$work/square.png" >/dev/null +iconset="$work/OpenMouse.iconset" +mkdir "$iconset" +for size in 16 32 128 256 512; do + sips -z $size $size "$work/square.png" --out "$iconset/icon_${size}x${size}.png" >/dev/null + sips -z $((size * 2)) $((size * 2)) "$work/square.png" --out "$iconset/icon_${size}x${size}@2x.png" >/dev/null +done +iconutil -c icns "$iconset" -o "$app/Contents/Resources/OpenMouse.icns" + +cat > "$app/Contents/Info.plist" < + + + + CFBundleNameOpenMouse + CFBundleDisplayNameOpenMouse Desktop + CFBundleIdentifierapp.openmouse.desktop + CFBundleExecutableopenmouse-gpui + CFBundleIconFileOpenMouse + CFBundlePackageTypeAPPL + CFBundleShortVersionString$version + CFBundleVersion$version + LSMinimumSystemVersion11.0 + NSHighResolutionCapable + + +PLIST + +# Ad-hoc signature: Apple Silicon refuses to run unsigned code. Not a +# substitute for a Developer ID signature and notarization. +codesign --force --deep --sign - "$app" + +hdiutil create -volname "OpenMouse" -srcfolder "$app" -ov -format UDZO "$out/OpenMouse.dmg" >/dev/null diff --git a/.github/workflows/build-gpui-macos.yml b/.github/workflows/build-gpui-macos.yml new file mode 100644 index 0000000..cef7c74 --- /dev/null +++ b/.github/workflows/build-gpui-macos.yml @@ -0,0 +1,56 @@ +name: Build (macOS) + +# Builds and tests the native GPUI app (gpui/) on macOS and uploads an +# ad-hoc-signed OpenMouse.dmg so any push can be downloaded and tried. Same +# triggers as the Windows build. Not yet part of release.yml. +on: + workflow_dispatch: + push: + branches: [main, gpui] + pull_request: + +concurrency: + group: build-macos-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: npm + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: gpui + shared-key: macos + + - name: Install JS dependencies + run: npm ci + + - name: Test + run: cargo test + working-directory: gpui + + # The faster `ci` profile (no LTO, see gpui/Cargo.toml). + - name: Build app + run: cargo build --profile ci + working-directory: gpui + + - name: Bundle OpenMouse.app + run: | + version=$(sed -n 's/^version = "\(.*\)"/\1/p' gpui/Cargo.toml | head -1) + mkdir dist + .github/scripts/make-macos-app.sh gpui/target/ci/openmouse-gpui "$version" dist + + - uses: actions/upload-artifact@v4 + with: + name: openmouse-macos + path: dist/OpenMouse.dmg + if-no-files-found: error diff --git a/.github/workflows/build-gpui-windows.yml b/.github/workflows/build-gpui-windows.yml index 0551682..bcbdd06 100644 --- a/.github/workflows/build-gpui-windows.yml +++ b/.github/workflows/build-gpui-windows.yml @@ -50,15 +50,19 @@ jobs: run: cargo test working-directory: gpui - - name: Build release binary - run: cargo build --release + # The faster `ci` profile (no LTO, see gpui/Cargo.toml); releases use + # the full `release` profile. + - name: Build app + run: cargo build --profile ci working-directory: gpui - # Needs the app's release .exe to exist first: its build.rs embeds - # those bytes as the installer's payload (see gpui/installer/build.rs). + # Needs the app's .exe to exist first: its build.rs embeds those bytes + # as the installer's payload (see gpui/installer/build.rs). - name: Build installer run: cargo build --release working-directory: gpui/installer + env: + OPENMOUSE_GPUI_EXE: ../target/ci/openmouse-gpui.exe - name: Rename installer artifact run: cp gpui/installer/target/release/openmouse-installer.exe OpenMouseDesktopSetup.exe diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index 3803f3f..572ccb5 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -27,6 +27,7 @@ openmouse-presence = { version = "0.0.1", path = "../crates/openmouse-presence" [target.'cfg(target_os = "macos")'.dependencies] cocoa = "=0.25.0" objc = "=0.2.7" +tray-icon = { version = "0.24", default-features = false } [dev-dependencies] gpui-kit = "=0.7.3" @@ -53,3 +54,11 @@ opt-level = 3 lto = true codegen-units = 1 strip = true + +# Pull-request and main builds only need to prove the app compiles and give +# testers a binary, so skip the whole-program LTO and single codegen unit that +# make `release` slow. Tagged releases still use `release`. +[profile.ci] +inherits = "release" +lto = false +codegen-units = 16 diff --git a/gpui/build.rs b/gpui/build.rs index 038c933..3e8a7e7 100644 --- a/gpui/build.rs +++ b/gpui/build.rs @@ -8,7 +8,9 @@ use std::process::Command; fn main() { // 16 MiB main-thread stack (Windows default is 1 MiB): a debug build of the // device page (dozens of controls in nested flex rows) overflows 1 MiB. - println!("cargo:rustc-link-arg-bins=/STACK:16777216"); + if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") { + println!("cargo:rustc-link-arg-bins=/STACK:16777216"); + } println!("cargo:rerun-if-changed=js"); println!("cargo:rerun-if-changed=js/build.mjs"); println!("cargo:rerun-if-changed=js/shims"); diff --git a/gpui/src/bin/main.rs b/gpui/src/bin/main.rs index 3952a03..b27f602 100644 --- a/gpui/src/bin/main.rs +++ b/gpui/src/bin/main.rs @@ -59,7 +59,6 @@ fn main() { if openmouse_gpui::autostart::launched_by_os() { let _ = cx.update_window(handle, |_, window, _| { if has_tray { - #[cfg(windows)] openmouse_gpui::tray::hide(window); } else { window.minimize_window(); diff --git a/gpui/src/tray.rs b/gpui/src/tray.rs index ba6e4e8..f9ce944 100644 --- a/gpui/src/tray.rs +++ b/gpui/src/tray.rs @@ -1,62 +1,110 @@ //! System tray: closing the main window hides it to a tray icon instead of //! quitting, and the icon brings it back. Quitting is the tray menu's job. //! -//! Windows only for now; elsewhere [`install`] does nothing and closing the -//! window quits as before. +//! Windows and macOS (menu bar); elsewhere [`install`] does nothing and closing +//! the window quits as before. -#[cfg(windows)] +#[cfg(any(windows, target_os = "macos"))] mod imp { use futures::channel::mpsc; use futures::StreamExt as _; use gpui_kit::{AnyWindowHandle, App, AppContext as _, Window}; - use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use tray_icon::menu::{Menu, MenuEvent, MenuItem, PredefinedMenuItem}; - use tray_icon::{Icon, MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; + use tray_icon::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; use crate::activity::ACTIVITY; - #[link(name = "user32")] - extern "system" { - fn ShowWindow(hwnd: isize, cmd: i32) -> i32; - fn SetForegroundWindow(hwnd: isize) -> i32; - } - - const SW_HIDE: i32 = 0; - /// Shows a hidden window and un-minimizes a minimized one. - const SW_RESTORE: i32 = 9; - /// The app icon `build.rs` embeds as the exe's first icon resource. - const APP_ICON_RESOURCE: u16 = 1; - enum Command { Show, Quit, } - fn hwnd_of(window: &Window) -> Option { - match HasWindowHandle::window_handle(window).map(|handle| handle.as_raw()) { - Ok(RawWindowHandle::Win32(handle)) => Some(handle.hwnd.get()), - _ => None, + #[cfg(windows)] + mod platform { + use gpui_kit::Window; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + use tray_icon::Icon; + + #[link(name = "user32")] + extern "system" { + fn ShowWindow(hwnd: isize, cmd: i32) -> i32; + fn SetForegroundWindow(hwnd: isize) -> i32; + } + + const SW_HIDE: i32 = 0; + /// Shows a hidden window and un-minimizes a minimized one. + const SW_RESTORE: i32 = 9; + /// The app icon `build.rs` embeds as the exe's first icon resource. + const APP_ICON_RESOURCE: u16 = 1; + + fn hwnd_of(window: &Window) -> Option { + match HasWindowHandle::window_handle(window).map(|handle| handle.as_raw()) { + Ok(RawWindowHandle::Win32(handle)) => Some(handle.hwnd.get()), + _ => None, + } + } + + pub fn icon() -> Option { + Icon::from_resource(APP_ICON_RESOURCE, Some((32, 32))).ok() + } + + /// Hides the window and its taskbar button. + pub fn hide_window(window: &Window) { + if let Some(hwnd) = hwnd_of(window) { + unsafe { + ShowWindow(hwnd, SW_HIDE); + } + } + } + + pub fn show_window(window: &mut Window) { + if let Some(hwnd) = hwnd_of(window) { + unsafe { + ShowWindow(hwnd, SW_RESTORE); + SetForegroundWindow(hwnd); + } + } } } - /// Hides the window (no taskbar button either) and tells the background - /// loops nobody is looking. - pub fn hide(window: &Window) { - if let Some(hwnd) = hwnd_of(window) { + #[cfg(target_os = "macos")] + mod platform { + use cocoa::base::{id, nil}; + use gpui_kit::Window; + use objc::{class, msg_send, sel, sel_impl}; + use tray_icon::Icon; + + pub fn icon() -> Option { + let image = image::load_from_memory(include_bytes!("../assets/logo.png")).ok()?.into_rgba8(); + let (width, height) = image.dimensions(); + Icon::from_rgba(image.into_raw(), width, height).ok() + } + + /// Hides the whole app (like Cmd+H); the menu bar icon stays. + pub fn hide_window(_window: &Window) { unsafe { - ShowWindow(hwnd, SW_HIDE); + let app: id = msg_send![class!(NSApplication), sharedApplication]; + let _: () = msg_send![app, hide: nil]; } } - ACTIVITY.set_active(false); - } - fn show(window: &mut Window) { - if let Some(hwnd) = hwnd_of(window) { + pub fn show_window(_window: &mut Window) { unsafe { - ShowWindow(hwnd, SW_RESTORE); - SetForegroundWindow(hwnd); + let app: id = msg_send![class!(NSApplication), sharedApplication]; + let _: () = msg_send![app, unhide: nil]; } } + } + + /// Hides the window (no taskbar button either) and tells the background + /// loops nobody is looking. + pub fn hide(window: &Window) { + platform::hide_window(window); + ACTIVITY.set_active(false); + } + + fn show(window: &mut Window) { + platform::show_window(window); window.activate_window(); ACTIVITY.set_active(true); } @@ -64,7 +112,7 @@ mod imp { /// Creates the tray icon and makes closing `window` hide it. Returns /// whether the tray exists; without one, closing still quits. pub fn install(cx: &mut App, window: AnyWindowHandle) -> bool { - let Ok(icon) = Icon::from_resource(APP_ICON_RESOURCE, Some((32, 32))) else { + let Some(icon) = platform::icon() else { return false; }; let open = MenuItem::with_id("open", "Open OpenMouse", true, None); @@ -132,10 +180,13 @@ mod imp { } } -#[cfg(windows)] +#[cfg(any(windows, target_os = "macos"))] pub use imp::{hide, install}; -#[cfg(not(windows))] +#[cfg(not(any(windows, target_os = "macos")))] pub fn install(_cx: &mut gpui_kit::App, _window: gpui_kit::AnyWindowHandle) -> bool { false } + +#[cfg(not(any(windows, target_os = "macos")))] +pub fn hide(_window: &gpui_kit::Window) {} From ad41e16dd2db243d8b9156843ceb89f7066794b2 Mon Sep 17 00:00:00 2001 From: "macroscopeapp[bot]" <170038800+macroscopeapp[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:27:27 +0000 Subject: [PATCH 2/2] Put OpenMouse.app at the root of the macOS DMG --- .github/scripts/make-macos-app.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/scripts/make-macos-app.sh b/.github/scripts/make-macos-app.sh index 1354d6a..da79aa8 100755 --- a/.github/scripts/make-macos-app.sh +++ b/.github/scripts/make-macos-app.sh @@ -47,4 +47,6 @@ PLIST # substitute for a Developer ID signature and notarization. codesign --force --deep --sign - "$app" -hdiutil create -volname "OpenMouse" -srcfolder "$app" -ov -format UDZO "$out/OpenMouse.dmg" >/dev/null +mkdir "$work/dmg-root" +cp -R "$app" "$work/dmg-root/" +hdiutil create -volname "OpenMouse" -srcfolder "$work/dmg-root" -ov -format UDZO "$out/OpenMouse.dmg" >/dev/null