1#![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
2
3use anyhow::{anyhow, Context, Result};
4use clap::Parser;
5use cli::{CliRequest, CliResponse};
6use serde::Deserialize;
7use std::{
8 env,
9 ffi::OsStr,
10 fs,
11 path::{Path, PathBuf},
12};
13use util::paths::PathLikeWithPosition;
14
15#[derive(Parser, Debug)]
16#[command(name = "zed", disable_version_flag = true)]
17struct Args {
18 /// Wait for all of the given paths to be opened/closed before exiting.
19 #[arg(short, long)]
20 wait: bool,
21 /// Add files to the currently open workspace
22 #[arg(short, long, overrides_with = "new")]
23 add: bool,
24 /// Create a new workspace
25 #[arg(short, long, overrides_with = "add")]
26 new: 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 #[arg(value_parser = parse_path_with_position)]
32 paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
33 /// Print Zed's version and the app path.
34 #[arg(short, long)]
35 version: bool,
36 /// Custom Zed.app path
37 #[arg(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 // Intercept version designators
57 #[cfg(target_os = "macos")]
58 if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
59 // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
60 use std::str::FromStr as _;
61
62 if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
63 return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
64 }
65 }
66 let args = Args::parse();
67
68 let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
69
70 if args.version {
71 println!("{}", bundle.zed_version_string());
72 return Ok(());
73 }
74
75 let curdir = env::current_dir()?;
76 let mut paths = vec![];
77 for path in args.paths_with_position {
78 let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
79 Ok(path) => Ok(path),
80 Err(e) => {
81 if let Some(mut parent) = path.parent() {
82 if parent == Path::new("") {
83 parent = &curdir;
84 }
85 match fs::canonicalize(parent) {
86 Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
87 Err(_) => Err(e),
88 }
89 } else {
90 Err(e)
91 }
92 }
93 })?;
94 paths.push(canonicalized.to_string(|path| path.display().to_string()))
95 }
96
97 let (tx, rx) = bundle.launch()?;
98 let open_new_workspace = if args.new {
99 Some(true)
100 } else if args.add {
101 Some(false)
102 } else {
103 None
104 };
105
106 tx.send(CliRequest::Open {
107 paths,
108 wait: args.wait,
109 open_new_workspace,
110 })?;
111
112 while let Ok(response) = rx.recv() {
113 match response {
114 CliResponse::Ping => {}
115 CliResponse::Stdout { message } => println!("{message}"),
116 CliResponse::Stderr { message } => eprintln!("{message}"),
117 CliResponse::Exit { status } => std::process::exit(status),
118 }
119 }
120
121 Ok(())
122}
123
124enum Bundle {
125 App {
126 app_bundle: PathBuf,
127 plist: InfoPlist,
128 },
129 LocalPath {
130 executable: PathBuf,
131 plist: InfoPlist,
132 },
133}
134
135fn locate_bundle() -> Result<PathBuf> {
136 let cli_path = std::env::current_exe()?.canonicalize()?;
137 let mut app_path = cli_path.clone();
138 while app_path.extension() != Some(OsStr::new("app")) {
139 if !app_path.pop() {
140 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
141 }
142 }
143 Ok(app_path)
144}
145
146#[cfg(target_os = "linux")]
147mod linux {
148 use std::path::Path;
149
150 use cli::{CliRequest, CliResponse};
151 use ipc_channel::ipc::{IpcReceiver, IpcSender};
152
153 use crate::{Bundle, InfoPlist};
154
155 impl Bundle {
156 pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
157 unimplemented!()
158 }
159
160 pub fn plist(&self) -> &InfoPlist {
161 unimplemented!()
162 }
163
164 pub fn path(&self) -> &Path {
165 unimplemented!()
166 }
167
168 pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
169 unimplemented!()
170 }
171
172 pub fn zed_version_string(&self) -> String {
173 unimplemented!()
174 }
175 }
176}
177
178// todo("windows")
179#[cfg(target_os = "windows")]
180mod windows {
181 use std::path::Path;
182
183 use cli::{CliRequest, CliResponse};
184 use ipc_channel::ipc::{IpcReceiver, IpcSender};
185
186 use crate::{Bundle, InfoPlist};
187
188 impl Bundle {
189 pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
190 unimplemented!()
191 }
192
193 pub fn plist(&self) -> &InfoPlist {
194 unimplemented!()
195 }
196
197 pub fn path(&self) -> &Path {
198 unimplemented!()
199 }
200
201 pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
202 unimplemented!()
203 }
204
205 pub fn zed_version_string(&self) -> String {
206 unimplemented!()
207 }
208 }
209}
210
211#[cfg(target_os = "macos")]
212mod mac_os {
213 use anyhow::{Context, Result};
214 use core_foundation::{
215 array::{CFArray, CFIndex},
216 string::kCFStringEncodingUTF8,
217 url::{CFURLCreateWithBytes, CFURL},
218 };
219 use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
220 use std::{fs, path::Path, ptr};
221
222 use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
223 use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
224
225 use crate::{locate_bundle, Bundle, InfoPlist};
226
227 impl Bundle {
228 pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
229 let bundle_path = if let Some(bundle_path) = args_bundle_path {
230 bundle_path
231 .canonicalize()
232 .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
233 } else {
234 locate_bundle().context("bundle autodiscovery")?
235 };
236
237 match bundle_path.extension().and_then(|ext| ext.to_str()) {
238 Some("app") => {
239 let plist_path = bundle_path.join("Contents/Info.plist");
240 let plist =
241 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
242 format!("Reading *.app bundle plist file at {plist_path:?}")
243 })?;
244 Ok(Self::App {
245 app_bundle: bundle_path,
246 plist,
247 })
248 }
249 _ => {
250 println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
251 let plist_path = bundle_path
252 .parent()
253 .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
254 .join("WebRTC.framework/Resources/Info.plist");
255 let plist =
256 plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
257 format!("Reading dev bundle plist file at {plist_path:?}")
258 })?;
259 Ok(Self::LocalPath {
260 executable: bundle_path,
261 plist,
262 })
263 }
264 }
265 }
266
267 fn plist(&self) -> &InfoPlist {
268 match self {
269 Self::App { plist, .. } => plist,
270 Self::LocalPath { plist, .. } => plist,
271 }
272 }
273
274 fn path(&self) -> &Path {
275 match self {
276 Self::App { app_bundle, .. } => app_bundle,
277 Self::LocalPath { executable, .. } => executable,
278 }
279 }
280
281 pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
282 let (server, server_name) =
283 IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
284 let url = format!("zed-cli://{server_name}");
285
286 match self {
287 Self::App { app_bundle, .. } => {
288 let app_path = app_bundle;
289
290 let status = unsafe {
291 let app_url = CFURL::from_path(app_path, true)
292 .with_context(|| format!("invalid app path {app_path:?}"))?;
293 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
294 ptr::null(),
295 url.as_ptr(),
296 url.len() as CFIndex,
297 kCFStringEncodingUTF8,
298 ptr::null(),
299 ));
300 // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
301 let urls_to_open =
302 CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
303 LSOpenFromURLSpec(
304 &LSLaunchURLSpec {
305 appURL: app_url.as_concrete_TypeRef(),
306 itemURLs: urls_to_open.as_concrete_TypeRef(),
307 passThruParams: ptr::null(),
308 launchFlags: kLSLaunchDefaults,
309 asyncRefCon: ptr::null_mut(),
310 },
311 ptr::null_mut(),
312 )
313 };
314
315 anyhow::ensure!(
316 status == 0,
317 "cannot start app bundle {}",
318 self.zed_version_string()
319 );
320 }
321
322 Self::LocalPath { executable, .. } => {
323 let executable_parent = executable
324 .parent()
325 .with_context(|| format!("Executable {executable:?} path has no parent"))?;
326 let subprocess_stdout_file = fs::File::create(
327 executable_parent.join("zed_dev.log"),
328 )
329 .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
330 let subprocess_stdin_file =
331 subprocess_stdout_file.try_clone().with_context(|| {
332 format!("Cloning descriptor for file {subprocess_stdout_file:?}")
333 })?;
334 let mut command = std::process::Command::new(executable);
335 let command = command
336 .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
337 .stderr(subprocess_stdout_file)
338 .stdout(subprocess_stdin_file)
339 .arg(url);
340
341 command
342 .spawn()
343 .with_context(|| format!("Spawning {command:?}"))?;
344 }
345 }
346
347 let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
348 Ok((handshake.requests, handshake.responses))
349 }
350
351 pub fn zed_version_string(&self) -> String {
352 let is_dev = matches!(self, Self::LocalPath { .. });
353 format!(
354 "Zed {}{} – {}",
355 self.plist().bundle_short_version_string,
356 if is_dev { " (dev)" } else { "" },
357 self.path().display(),
358 )
359 }
360 }
361 pub(super) fn spawn_channel_cli(
362 channel: release_channel::ReleaseChannel,
363 leftover_args: Vec<String>,
364 ) -> Result<()> {
365 use anyhow::bail;
366 use std::process::Command;
367
368 let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
369 let app_id_output = Command::new("osascript")
370 .arg("-e")
371 .arg(&app_id_prompt)
372 .output()?;
373 if !app_id_output.status.success() {
374 bail!("Could not determine app id for {}", channel.display_name());
375 }
376 let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
377 let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
378 let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
379 if !app_path_output.status.success() {
380 bail!(
381 "Could not determine app path for {}",
382 channel.display_name()
383 );
384 }
385 let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
386 let cli_path = format!("{app_path}/Contents/MacOS/cli");
387 Command::new(cli_path).args(leftover_args).spawn()?;
388 Ok(())
389 }
390}