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