1use anyhow::{anyhow, Context, Result};
2use clap::Parser;
3use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
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 serde::Deserialize;
12use std::{
13 ffi::OsStr,
14 fs::{self, OpenOptions},
15 io,
16 path::{Path, PathBuf},
17 ptr,
18};
19use util::paths::PathLikeWithPosition;
20
21#[derive(Parser)]
22#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
23struct Args {
24 /// Wait for all of the given paths to be opened/closed before exiting.
25 #[clap(short, long)]
26 wait: bool,
27 /// A sequence of space-separated paths that you want to open.
28 ///
29 /// Use `path:line:row` syntax to open a file at a specific location.
30 /// Non-existing paths and directories will ignore `:line:row` suffix.
31 #[clap(value_parser = parse_path_with_position)]
32 paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
33 /// Print Zed's version and the app path.
34 #[clap(short, long)]
35 version: bool,
36 /// Custom Zed.app path
37 #[clap(short, long)]
38 bundle_path: Option<PathBuf>,
39}
40
41fn parse_path_with_position(
42 argument_str: &str,
43) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
44 PathLikeWithPosition::parse_str(argument_str, |path_str| {
45 Ok(Path::new(path_str).to_path_buf())
46 })
47}
48
49#[derive(Debug, Deserialize)]
50struct InfoPlist {
51 #[serde(rename = "CFBundleShortVersionString")]
52 bundle_short_version_string: String,
53}
54
55fn main() -> Result<()> {
56 let args = Args::parse();
57
58 let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
59
60 if args.version {
61 println!("{}", bundle.zed_version_string());
62 return Ok(());
63 }
64
65 for path in args
66 .paths_with_position
67 .iter()
68 .map(|path_with_position| &path_with_position.path_like)
69 {
70 if !path.exists() {
71 touch(path.as_path())?;
72 }
73 }
74
75 let (tx, rx) = bundle.launch()?;
76
77 tx.send(CliRequest::Open {
78 paths: args
79 .paths_with_position
80 .into_iter()
81 .map(|path_with_position| {
82 let path_with_position = path_with_position.map_path_like(|path| {
83 fs::canonicalize(&path)
84 .with_context(|| format!("path {path:?} canonicalization"))
85 })?;
86 Ok(path_with_position.to_string(|path| path.display().to_string()))
87 })
88 .collect::<Result<_>>()?,
89 wait: args.wait,
90 })?;
91
92 while let Ok(response) = rx.recv() {
93 match response {
94 CliResponse::Ping => {}
95 CliResponse::Stdout { message } => println!("{message}"),
96 CliResponse::Stderr { message } => eprintln!("{message}"),
97 CliResponse::Exit { status } => std::process::exit(status),
98 }
99 }
100
101 Ok(())
102}
103
104enum Bundle {
105 App {
106 app_bundle: PathBuf,
107 plist: InfoPlist,
108 },
109 LocalPath {
110 executable: PathBuf,
111 plist: InfoPlist,
112 },
113}
114
115impl Bundle {
116 fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
117 let bundle_path = if let Some(bundle_path) = args_bundle_path {
118 bundle_path
119 .canonicalize()
120 .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
121 } else {
122 locate_bundle().context("bundle autodiscovery")?
123 };
124
125 match bundle_path.extension().and_then(|ext| ext.to_str()) {
126 Some("app") => {
127 let plist_path = bundle_path.join("Contents/Info.plist");
128 let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
129 format!("Reading *.app bundle plist file at {plist_path:?}")
130 })?;
131 Ok(Self::App {
132 app_bundle: bundle_path,
133 plist,
134 })
135 }
136 _ => {
137 println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
138 let plist_path = bundle_path
139 .parent()
140 .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
141 .join("WebRTC.framework/Resources/Info.plist");
142 let plist = plist::from_file::<_, InfoPlist>(&plist_path)
143 .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
144 Ok(Self::LocalPath {
145 executable: bundle_path,
146 plist,
147 })
148 }
149 }
150 }
151
152 fn plist(&self) -> &InfoPlist {
153 match self {
154 Self::App { plist, .. } => plist,
155 Self::LocalPath { plist, .. } => plist,
156 }
157 }
158
159 fn path(&self) -> &Path {
160 match self {
161 Self::App { app_bundle, .. } => app_bundle,
162 Self::LocalPath { executable, .. } => executable,
163 }
164 }
165
166 fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
167 let (server, server_name) =
168 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
169 let url = format!("zed-cli://{server_name}");
170
171 match self {
172 Self::App { app_bundle, .. } => {
173 let app_path = app_bundle;
174
175 let status = unsafe {
176 let app_url = CFURL::from_path(app_path, true)
177 .with_context(|| format!("invalid app path {app_path:?}"))?;
178 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
179 ptr::null(),
180 url.as_ptr(),
181 url.len() as CFIndex,
182 kCFStringEncodingUTF8,
183 ptr::null(),
184 ));
185 // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
186 let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
187 LSOpenFromURLSpec(
188 &LSLaunchURLSpec {
189 appURL: app_url.as_concrete_TypeRef(),
190 itemURLs: urls_to_open.as_concrete_TypeRef(),
191 passThruParams: ptr::null(),
192 launchFlags: kLSLaunchDefaults,
193 asyncRefCon: ptr::null_mut(),
194 },
195 ptr::null_mut(),
196 )
197 };
198
199 anyhow::ensure!(
200 status == 0,
201 "cannot start app bundle {}",
202 self.zed_version_string()
203 );
204 }
205
206 Self::LocalPath { executable, .. } => {
207 let executable_parent = executable
208 .parent()
209 .with_context(|| format!("Executable {executable:?} path has no parent"))?;
210 let subprocess_stdout_file =
211 fs::File::create(executable_parent.join("zed_dev.log"))
212 .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
213 let subprocess_stdin_file =
214 subprocess_stdout_file.try_clone().with_context(|| {
215 format!("Cloning descriptor for file {subprocess_stdout_file:?}")
216 })?;
217 let mut command = std::process::Command::new(executable);
218 let command = command
219 .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
220 .stderr(subprocess_stdout_file)
221 .stdout(subprocess_stdin_file)
222 .arg(url);
223
224 command
225 .spawn()
226 .with_context(|| format!("Spawning {command:?}"))?;
227 }
228 }
229
230 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
231 Ok((handshake.requests, handshake.responses))
232 }
233
234 fn zed_version_string(&self) -> String {
235 let is_dev = matches!(self, Self::LocalPath { .. });
236 format!(
237 "Zed {}{} – {}",
238 self.plist().bundle_short_version_string,
239 if is_dev { " (dev)" } else { "" },
240 self.path().display(),
241 )
242 }
243}
244
245fn touch(path: &Path) -> io::Result<()> {
246 match OpenOptions::new().create(true).write(true).open(path) {
247 Ok(_) => Ok(()),
248 Err(e) => Err(e),
249 }
250}
251
252fn locate_bundle() -> Result<PathBuf> {
253 let cli_path = std::env::current_exe()?.canonicalize()?;
254 let mut app_path = cli_path.clone();
255 while app_path.extension() != Some(OsStr::new("app")) {
256 if !app_path.pop() {
257 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
258 }
259 }
260 Ok(app_path)
261}