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