main.rs

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