1use anyhow::{anyhow, Result};
2use clap::Parser;
3use cli::{CliRequest, CliResponse, IpcHandshake};
4use core_foundation::{
5 array::{CFArray, CFIndex},
6 string::kCFStringEncodingUTF8,
7 url::{CFURLCreateWithBytes, CFURL},
8};
9use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
10use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
11use objc::{class, msg_send, sel, sel_impl};
12use std::{ffi::CStr, fs, path::PathBuf, ptr};
13
14#[derive(Parser)]
15#[clap(name = "zed")]
16struct Args {
17 /// Wait for all of the given paths to be closed before exiting.
18 #[clap(short, long)]
19 wait: bool,
20 /// A sequence of space-separated paths that you want to open.
21 #[clap()]
22 paths: Vec<PathBuf>,
23}
24
25fn main() -> Result<()> {
26 let args = Args::parse();
27
28 let app_path = locate_app()?;
29 let (tx, rx) = launch_app(app_path)?;
30
31 tx.send(CliRequest::Open {
32 paths: args
33 .paths
34 .into_iter()
35 .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
36 .collect::<Result<Vec<PathBuf>>>()?,
37 wait: args.wait,
38 })?;
39
40 while let Ok(response) = rx.recv() {
41 match response {
42 CliResponse::Ping => {}
43 CliResponse::Stdout { message } => println!("{message}"),
44 CliResponse::Stderr { message } => eprintln!("{message}"),
45 CliResponse::Exit { status } => std::process::exit(status),
46 }
47 }
48
49 Ok(())
50}
51
52fn locate_app() -> Result<PathBuf> {
53 if cfg!(debug_assertions) {
54 Ok(std::env::current_exe()?
55 .parent()
56 .unwrap()
57 .join("bundle/osx/Zed.app"))
58 } else {
59 Ok(path_to_app_with_bundle_identifier("dev.zed.Zed")
60 .unwrap_or_else(|| "/Applications/Zed.dev".into()))
61 }
62}
63
64fn path_to_app_with_bundle_identifier(bundle_id: &str) -> Option<PathBuf> {
65 use cocoa::{
66 base::{id, nil},
67 foundation::{NSString, NSURL as _},
68 };
69
70 unsafe {
71 let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
72 let bundle_id = NSString::alloc(nil).init_str(bundle_id);
73 let app_url: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id];
74 if !app_url.is_null() {
75 Some(PathBuf::from(
76 CStr::from_ptr(app_url.path().UTF8String())
77 .to_string_lossy()
78 .to_string(),
79 ))
80 } else {
81 None
82 }
83 }
84}
85
86fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
87 let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
88 let url = format!("zed-cli://{server_name}");
89
90 let status = unsafe {
91 let app_url =
92 CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
93 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
94 ptr::null(),
95 url.as_ptr(),
96 url.len() as CFIndex,
97 kCFStringEncodingUTF8,
98 ptr::null(),
99 ));
100 let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
101 LSOpenFromURLSpec(
102 &LSLaunchURLSpec {
103 appURL: app_url.as_concrete_TypeRef(),
104 itemURLs: urls_to_open.as_concrete_TypeRef(),
105 passThruParams: ptr::null(),
106 launchFlags: kLSLaunchDefaults,
107 asyncRefCon: ptr::null_mut(),
108 },
109 ptr::null_mut(),
110 )
111 };
112
113 if status == 0 {
114 let (_, handshake) = server.accept()?;
115 Ok((handshake.requests, handshake.responses))
116 } else {
117 Err(anyhow!("cannot start {:?}", app_path))
118 }
119}