Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/dv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ gui = [
"dep:gpui",
"dep:gpui_platform",
"dep:libc",
"dep:objc2",
"dep:velopack",
"dep:windows-sys",
]
Expand Down Expand Up @@ -118,6 +119,9 @@ gpui_platform = { workspace = true, optional = true, features = ["wayland", "x11
# Metal toolchain (Xcode's is a separate download).
[target.'cfg(target_os = "macos")'.dependencies]
gpui_platform = { workspace = true, optional = true, features = ["font-kit", "runtime_shaders"] }
# The app's icon in the Dock and the app switcher when it runs outside an app bundle
# (`dock_icon.rs`, PLAN §13.1): the app's, never the headless build's.
objc2 = { workspace = true, optional = true }

# dv.exe's and dv-gui.exe's icon and version resource on Windows (`build.rs`); a no-op elsewhere.
[build-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions crates/dv/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ fn start(startup: StartOptions, cx: &mut App) -> Result<()> {
// An installed copy's updates, in the background (PLAN D9); none under automation.
#[cfg(any(windows, target_os = "macos"))]
crate::updates::start(automation, main.shell.clone(), cx);
// A development build has no bundle to take its Dock icon from.
#[cfg(target_os = "macos")]
crate::dock_icon::set_unless_bundled();
// A background run must never take the foreground from the person at the machine.
if !dv_ui::automation::is_background() {
cx.activate(true);
Expand Down
75 changes: 75 additions & 0 deletions crates/dv/src/dock_icon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! The app's icon in the Dock and the app switcher on macOS when dv runs outside an app bundle
//! (a development build, `cargo run`, `target/release/dv`; PLAN §13.1). AppKit takes an app's
//! icon from its bundle's `CFBundleIconFile`, so a bare executable shows the generic one; this
//! hands AppKit the bundled icon itself (`-[NSApplication setApplicationIconImage:]`), the same
//! `resources/macos/dv.icns` the packaged app's bundle carries. A packaged build runs from its
//! bundle and is left alone.

use std::ffi::{CStr, c_char, c_void};

use objc2::runtime::AnyObject;
use objc2::{class, msg_send};

/// The icon the packaged app's bundle carries (`script/package`).
const ICNS: &[u8] = include_bytes!("../resources/macos/dv.icns");

/// Gives the app the bundled icon when it isn't running from an app bundle (module docs).
/// Nothing happens when AppKit can't read it.
pub fn set_unless_bundled() {
// SAFETY: plain AppKit and Foundation messages on the main thread, where gpui runs the app;
// every object returned is checked for nil before use, and `ICNS` outlives the copy
// `dataWithBytes:length:` makes of it.
unsafe {
let bundle: *mut AnyObject = msg_send![class!(NSBundle), mainBundle];
if bundle.is_null() {
return;
}
let path: *mut AnyObject = msg_send![bundle, bundlePath];
if !path.is_null() {
let utf8: *const c_char = msg_send![path, UTF8String];
if !utf8.is_null() && is_app_bundle(&CStr::from_ptr(utf8).to_string_lossy()) {
return;
}
}
let data: *mut AnyObject = msg_send![
class!(NSData),
dataWithBytes: ICNS.as_ptr().cast::<c_void>(),
length: ICNS.len()
];
if data.is_null() {
return;
}
let image: *mut AnyObject = msg_send![class!(NSImage), alloc];
let image: *mut AnyObject = msg_send![image, initWithData: data];
if image.is_null() {
log::warn!("AppKit couldn't read the app icon");
return;
}
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let _: () = msg_send![app, setApplicationIconImage: image];
}
}

/// Whether `bundle_path` (`-[NSBundle bundlePath]` of the main bundle) is an app bundle: for a
/// bare executable it is the executable's folder.
fn is_app_bundle(bundle_path: &str) -> bool {
bundle_path.trim_end_matches('/').ends_with(".app")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn only_an_app_bundle_keeps_its_own_icon() {
assert!(is_app_bundle("/Applications/dv.app"));
assert!(is_app_bundle("/Applications/dv.app/"));
assert!(!is_app_bundle("/Users/kyle/code/dv/target/release"));
assert!(!is_app_bundle("/Users/kyle/apps"));
}

#[test]
fn the_embedded_icon_is_an_icns_file() {
assert_eq!(&ICNS[..4], b"icns");
}
}
2 changes: 2 additions & 0 deletions crates/dv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ mod cli;
mod crash;
#[cfg(feature = "gui")]
mod data_dir;
#[cfg(all(target_os = "macos", feature = "gui"))]
mod dock_icon;
#[cfg(feature = "gui")]
mod fatal;
#[cfg(all(any(windows, target_os = "macos"), feature = "gui"))]
Expand Down
4 changes: 4 additions & 0 deletions docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -2352,6 +2352,10 @@ setup**.
`dv.exe`'s, and `--help` / `--version` show their text in an information box and exit 0. Both carry dv's
icon (DESIGN §1.7) as resource 1, which gpui loads as the window's icon, and a version
resource (`crates/dv/build.rs`, `embed-resource`, Windows only).
On macOS an app's Dock and app-switcher icon comes from its bundle, which only the packaged app
has, so a development build run outside one (`cargo run`, `target/release/dv`) gives AppKit the
bundle's icon itself at startup (`dock_icon.rs`: `crates/dv/resources/macos/dv.icns` through
`-[NSApplication setApplicationIconImage:]`), leaving a bundled app's alone.
A Windows child inherits every inheritable handle of its parent, the caller's stdout pipe
among them, so the first makes its own standard handles non-inheritable before it starts the
second: a caller capturing the output (`$(dv open .)`, an agent's shell tool) sees its end at
Expand Down