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                let path_with_position = path_with_position.map_path_like(|path| {
 83                    fs::canonicalize(&path)
 84                        .with_context(|| format!("path {path:?} canonicalization"))
 85                })?;
 86                Ok(path_with_position.to_string(|path| path.display().to_string()))
 87            })
 88            .collect::<Result<_>>()?,
 89        wait: args.wait,
 90    })?;
 91
 92    while let Ok(response) = rx.recv() {
 93        match response {
 94            CliResponse::Ping => {}
 95            CliResponse::Stdout { message } => println!("{message}"),
 96            CliResponse::Stderr { message } => eprintln!("{message}"),
 97            CliResponse::Exit { status } => std::process::exit(status),
 98        }
 99    }
100
101    Ok(())
102}
103
104enum Bundle {
105    App {
106        app_bundle: PathBuf,
107        plist: InfoPlist,
108    },
109    LocalPath {
110        executable: PathBuf,
111        plist: InfoPlist,
112    },
113}
114
115impl Bundle {
116    fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
117        let bundle_path = if let Some(bundle_path) = args_bundle_path {
118            bundle_path
119                .canonicalize()
120                .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
121        } else {
122            locate_bundle().context("bundle autodiscovery")?
123        };
124
125        match bundle_path.extension().and_then(|ext| ext.to_str()) {
126            Some("app") => {
127                let plist_path = bundle_path.join("Contents/Info.plist");
128                let plist = plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
129                    format!("Reading *.app bundle plist file at {plist_path:?}")
130                })?;
131                Ok(Self::App {
132                    app_bundle: bundle_path,
133                    plist,
134                })
135            }
136            _ => {
137                println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
138                let plist_path = bundle_path
139                    .parent()
140                    .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
141                    .join("WebRTC.framework/Resources/Info.plist");
142                let plist = plist::from_file::<_, InfoPlist>(&plist_path)
143                    .with_context(|| format!("Reading dev bundle plist file at {plist_path:?}"))?;
144                Ok(Self::LocalPath {
145                    executable: bundle_path,
146                    plist,
147                })
148            }
149        }
150    }
151
152    fn plist(&self) -> &InfoPlist {
153        match self {
154            Self::App { plist, .. } => plist,
155            Self::LocalPath { plist, .. } => plist,
156        }
157    }
158
159    fn path(&self) -> &Path {
160        match self {
161            Self::App { app_bundle, .. } => app_bundle,
162            Self::LocalPath { executable, .. } => executable,
163        }
164    }
165
166    fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
167        let (server, server_name) =
168            IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
169        let url = format!("zed-cli://{server_name}");
170
171        match self {
172            Self::App { app_bundle, .. } => {
173                let app_path = app_bundle;
174
175                let status = unsafe {
176                    let app_url = CFURL::from_path(app_path, true)
177                        .with_context(|| format!("invalid app path {app_path:?}"))?;
178                    let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
179                        ptr::null(),
180                        url.as_ptr(),
181                        url.len() as CFIndex,
182                        kCFStringEncodingUTF8,
183                        ptr::null(),
184                    ));
185                    let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
186                    LSOpenFromURLSpec(
187                        &LSLaunchURLSpec {
188                            appURL: app_url.as_concrete_TypeRef(),
189                            itemURLs: urls_to_open.as_concrete_TypeRef(),
190                            passThruParams: ptr::null(),
191                            launchFlags: kLSLaunchDefaults,
192                            asyncRefCon: ptr::null_mut(),
193                        },
194                        ptr::null_mut(),
195                    )
196                };
197
198                anyhow::ensure!(
199                    status == 0,
200                    "cannot start app bundle {}",
201                    self.zed_version_string()
202                );
203            }
204
205            Self::LocalPath { executable, .. } => {
206                let executable_parent = executable
207                    .parent()
208                    .with_context(|| format!("Executable {executable:?} path has no parent"))?;
209                let subprocess_stdout_file =
210                    fs::File::create(executable_parent.join("zed_dev.log"))
211                        .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
212                let subprocess_stdin_file =
213                    subprocess_stdout_file.try_clone().with_context(|| {
214                        format!("Cloning descriptor for file {subprocess_stdout_file:?}")
215                    })?;
216                let mut command = std::process::Command::new(executable);
217                let command = command
218                    .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
219                    .stderr(subprocess_stdout_file)
220                    .stdout(subprocess_stdin_file)
221                    .arg(url);
222
223                command
224                    .spawn()
225                    .with_context(|| format!("Spawning {command:?}"))?;
226            }
227        }
228
229        let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
230        Ok((handshake.requests, handshake.responses))
231    }
232
233    fn zed_version_string(&self) -> String {
234        let is_dev = matches!(self, Self::LocalPath { .. });
235        format!(
236            "Zed {}{}{}",
237            self.plist().bundle_short_version_string,
238            if is_dev { " (dev)" } else { "" },
239            self.path().display(),
240        )
241    }
242}
243
244fn touch(path: &Path) -> io::Result<()> {
245    match OpenOptions::new().create(true).write(true).open(path) {
246        Ok(_) => Ok(()),
247        Err(e) => Err(e),
248    }
249}
250
251fn locate_bundle() -> Result<PathBuf> {
252    let cli_path = std::env::current_exe()?.canonicalize()?;
253    let mut app_path = cli_path.clone();
254    while app_path.extension() != Some(OsStr::new("app")) {
255        if !app_path.pop() {
256            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
257        }
258    }
259    Ok(app_path)
260}