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};
 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                path_with_position.convert_path(|path| {
 83                    fs::canonicalize(&path)
 84                        .with_context(|| format!("path {path:?} canonicalization"))
 85                })
 86            })
 87            .collect::<Result<_>>()?,
 88        wait: args.wait,
 89    })?;
 90
 91    while let Ok(response) = rx.recv() {
 92        match response {
 93            CliResponse::Ping => {}
 94            CliResponse::Stdout { message } => println!("{message}"),
 95            CliResponse::Stderr { message } => eprintln!("{message}"),
 96            CliResponse::Exit { status } => std::process::exit(status),
 97        }
 98    }
 99
100    Ok(())
101}
102
103enum Bundle {
104    App {
105        app_bundle: PathBuf,
106        plist: InfoPlist,
107    },
108    LocalPath {
109        executable: PathBuf,
110        plist: InfoPlist,
111    },
112}
113
114impl Bundle {
115    fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
116        let bundle_path = if let Some(bundle_path) = args_bundle_path {
117            bundle_path
118                .canonicalize()
119                .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
120        } else {
121            locate_bundle().context("bundle autodiscovery")?
122        };
123
124        match bundle_path.extension().and_then(|ext| ext.to_str()) {
125            Some("app") => {
126                let plist_path = bundle_path.join("Contents/Info.plist");
127                let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
128                    format!("Reading *.app bundle plist file at {plist_path:?}")
129                })?;
130                Ok(Self::App {
131                    app_bundle: bundle_path,
132                    plist,
133                })
134            }
135            _ => {
136                println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
137                let plist_path = bundle_path
138                    .parent()
139                    .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
140                    .join("WebRTC.framework/Resources/Info.plist");
141                let plist = plist::from_file::<_, InfoPlist>(&plist_path)
142                    .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
143                Ok(Self::LocalPath {
144                    executable: bundle_path,
145                    plist,
146                })
147            }
148        }
149    }
150
151    fn plist(&self) -> &InfoPlist {
152        match self {
153            Self::App { plist, .. } => plist,
154            Self::LocalPath { plist, .. } => plist,
155        }
156    }
157
158    fn path(&self) -> &Path {
159        match self {
160            Self::App { app_bundle, .. } => app_bundle,
161            Self::LocalPath {
162                executable: excutable,
163                ..
164            } => excutable,
165        }
166    }
167
168    fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
169        let (server, server_name) =
170            IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
171        let url = format!("zed-cli://{server_name}");
172
173        match self {
174            Self::App { app_bundle, .. } => {
175                let app_path = app_bundle;
176
177                let status = unsafe {
178                    let app_url = CFURL::from_path(app_path, true)
179                        .with_context(|| format!("invalid app path {app_path:?}"))?;
180                    let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
181                        ptr::null(),
182                        url.as_ptr(),
183                        url.len() as CFIndex,
184                        kCFStringEncodingUTF8,
185                        ptr::null(),
186                    ));
187                    let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
188                    LSOpenFromURLSpec(
189                        &LSLaunchURLSpec {
190                            appURL: app_url.as_concrete_TypeRef(),
191                            itemURLs: urls_to_open.as_concrete_TypeRef(),
192                            passThruParams: ptr::null(),
193                            launchFlags: kLSLaunchDefaults,
194                            asyncRefCon: ptr::null_mut(),
195                        },
196                        ptr::null_mut(),
197                    )
198                };
199
200                anyhow::ensure!(
201                    status == 0,
202                    "cannot start app bundle {}",
203                    self.zed_version_string()
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}