main.rs

  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}