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: false,
38 })?;
39
40 while let Ok(response) = rx.recv() {
41 match response {
42 CliResponse::Stdout { message } => println!("{message}"),
43 CliResponse::Stderr { message } => eprintln!("{message}"),
44 CliResponse::Exit { status } => std::process::exit(status),
45 }
46 }
47
48 Ok(())
49}
50
51fn locate_app() -> Result<PathBuf> {
52 if cfg!(debug_assertions) {
53 Ok(std::env::current_exe()?
54 .parent()
55 .unwrap()
56 .join("bundle/osx/Zed.app"))
57 } else {
58 Ok(path_to_app_with_bundle_identifier("dev.zed.Zed")
59 .unwrap_or_else(|| "/Applications/Zed.dev".into()))
60 }
61}
62
63fn path_to_app_with_bundle_identifier(bundle_id: &str) -> Option<PathBuf> {
64 use cocoa::{
65 base::{id, nil},
66 foundation::{NSString, NSURL as _},
67 };
68
69 unsafe {
70 let workspace: id = msg_send![class!(NSWorkspace), sharedWorkspace];
71 let bundle_id = NSString::alloc(nil).init_str(bundle_id);
72 let app_url: id = msg_send![workspace, URLForApplicationWithBundleIdentifier: bundle_id];
73 if !app_url.is_null() {
74 Some(PathBuf::from(
75 CStr::from_ptr(app_url.path().UTF8String())
76 .to_string_lossy()
77 .to_string(),
78 ))
79 } else {
80 None
81 }
82 }
83}
84
85fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
86 let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
87 let url = format!("zed-cli://{server_name}");
88
89 let status = unsafe {
90 let app_url =
91 CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
92 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
93 ptr::null(),
94 url.as_ptr(),
95 url.len() as CFIndex,
96 kCFStringEncodingUTF8,
97 ptr::null(),
98 ));
99 let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
100 LSOpenFromURLSpec(
101 &LSLaunchURLSpec {
102 appURL: app_url.as_concrete_TypeRef(),
103 itemURLs: urls_to_open.as_concrete_TypeRef(),
104 passThruParams: ptr::null(),
105 launchFlags: kLSLaunchDefaults,
106 asyncRefCon: ptr::null_mut(),
107 },
108 ptr::null_mut(),
109 )
110 };
111
112 if status == 0 {
113 let (_, handshake) = server.accept()?;
114 Ok((handshake.requests, handshake.responses))
115 } else {
116 Err(anyhow!("cannot start {:?}", app_path))
117 }
118}