main.rs

  1#![cfg_attr(target_os = "linux", allow(dead_code))]
  2
  3use anyhow::{anyhow, Context, Result};
  4use clap::Parser;
  5use cli::{CliRequest, CliResponse};
  6use serde::Deserialize;
  7use std::{
  8    ffi::OsStr,
  9    fs::{self, OpenOptions},
 10    io,
 11    path::{Path, PathBuf},
 12};
 13use util::paths::PathLikeWithPosition;
 14
 15#[derive(Parser)]
 16#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
 17struct Args {
 18    /// Wait for all of the given paths to be opened/closed before exiting.
 19    #[clap(short, long)]
 20    wait: bool,
 21    /// A sequence of space-separated paths that you want to open.
 22    ///
 23    /// Use `path:line:row` syntax to open a file at a specific location.
 24    /// Non-existing paths and directories will ignore `:line:row` suffix.
 25    #[clap(value_parser = parse_path_with_position)]
 26    paths_with_position: Vec<PathLikeWithPosition<PathBuf>>,
 27    /// Print Zed's version and the app path.
 28    #[clap(short, long)]
 29    version: bool,
 30    /// Custom Zed.app path
 31    #[clap(short, long)]
 32    bundle_path: Option<PathBuf>,
 33}
 34
 35fn parse_path_with_position(
 36    argument_str: &str,
 37) -> Result<PathLikeWithPosition<PathBuf>, std::convert::Infallible> {
 38    PathLikeWithPosition::parse_str(argument_str, |path_str| {
 39        Ok(Path::new(path_str).to_path_buf())
 40    })
 41}
 42
 43#[derive(Debug, Deserialize)]
 44struct InfoPlist {
 45    #[serde(rename = "CFBundleShortVersionString")]
 46    bundle_short_version_string: String,
 47}
 48
 49fn main() -> Result<()> {
 50    let args = Args::parse();
 51
 52    let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
 53
 54    if args.version {
 55        println!("{}", bundle.zed_version_string());
 56        return Ok(());
 57    }
 58
 59    for path in args
 60        .paths_with_position
 61        .iter()
 62        .map(|path_with_position| &path_with_position.path_like)
 63    {
 64        if !path.exists() {
 65            touch(path.as_path())?;
 66        }
 67    }
 68
 69    let (tx, rx) = bundle.launch()?;
 70
 71    tx.send(CliRequest::Open {
 72        paths: args
 73            .paths_with_position
 74            .into_iter()
 75            .map(|path_with_position| {
 76                let path_with_position = path_with_position.map_path_like(|path| {
 77                    fs::canonicalize(&path)
 78                        .with_context(|| format!("path {path:?} canonicalization"))
 79                })?;
 80                Ok(path_with_position.to_string(|path| path.display().to_string()))
 81            })
 82            .collect::<Result<_>>()?,
 83        wait: args.wait,
 84    })?;
 85
 86    while let Ok(response) = rx.recv() {
 87        match response {
 88            CliResponse::Ping => {}
 89            CliResponse::Stdout { message } => println!("{message}"),
 90            CliResponse::Stderr { message } => eprintln!("{message}"),
 91            CliResponse::Exit { status } => std::process::exit(status),
 92        }
 93    }
 94
 95    Ok(())
 96}
 97
 98enum Bundle {
 99    App {
100        app_bundle: PathBuf,
101        plist: InfoPlist,
102    },
103    LocalPath {
104        executable: PathBuf,
105        plist: InfoPlist,
106    },
107}
108
109fn touch(path: &Path) -> io::Result<()> {
110    match OpenOptions::new().create(true).write(true).open(path) {
111        Ok(_) => Ok(()),
112        Err(e) => Err(e),
113    }
114}
115
116fn locate_bundle() -> Result<PathBuf> {
117    let cli_path = std::env::current_exe()?.canonicalize()?;
118    let mut app_path = cli_path.clone();
119    while app_path.extension() != Some(OsStr::new("app")) {
120        if !app_path.pop() {
121            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
122        }
123    }
124    Ok(app_path)
125}
126
127#[cfg(target_os = "linux")]
128mod linux {
129    use std::path::Path;
130
131    use cli::{CliRequest, CliResponse};
132    use ipc_channel::ipc::{IpcReceiver, IpcSender};
133
134    use crate::{Bundle, InfoPlist};
135
136    impl Bundle {
137        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
138            unimplemented!()
139        }
140
141        pub fn plist(&self) -> &InfoPlist {
142            unimplemented!()
143        }
144
145        pub fn path(&self) -> &Path {
146            unimplemented!()
147        }
148
149        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
150            unimplemented!()
151        }
152
153        pub fn zed_version_string(&self) -> String {
154            unimplemented!()
155        }
156    }
157}
158
159#[cfg(target_os = "macos")]
160mod mac_os {
161    use anyhow::Context;
162    use core_foundation::{
163        array::{CFArray, CFIndex},
164        string::kCFStringEncodingUTF8,
165        url::{CFURLCreateWithBytes, CFURL},
166    };
167    use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
168    use std::{fs, path::Path, ptr};
169
170    use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
171    use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
172
173    use crate::{locate_bundle, Bundle, InfoPlist};
174
175    impl Bundle {
176        pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
177            let bundle_path = if let Some(bundle_path) = args_bundle_path {
178                bundle_path
179                    .canonicalize()
180                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
181            } else {
182                locate_bundle().context("bundle autodiscovery")?
183            };
184
185            match bundle_path.extension().and_then(|ext| ext.to_str()) {
186                Some("app") => {
187                    let plist_path = bundle_path.join("Contents/Info.plist");
188                    let plist =
189                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
190                            format!("Reading *.app bundle plist file at {plist_path:?}")
191                        })?;
192                    Ok(Self::App {
193                        app_bundle: bundle_path,
194                        plist,
195                    })
196                }
197                _ => {
198                    println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
199                    let plist_path = bundle_path
200                        .parent()
201                        .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
202                        .join("WebRTC.framework/Resources/Info.plist");
203                    let plist =
204                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
205                            format!("Reading dev bundle plist file at {plist_path:?}")
206                        })?;
207                    Ok(Self::LocalPath {
208                        executable: bundle_path,
209                        plist,
210                    })
211                }
212            }
213        }
214
215        fn plist(&self) -> &InfoPlist {
216            match self {
217                Self::App { plist, .. } => plist,
218                Self::LocalPath { plist, .. } => plist,
219            }
220        }
221
222        fn path(&self) -> &Path {
223            match self {
224                Self::App { app_bundle, .. } => app_bundle,
225                Self::LocalPath { executable, .. } => executable,
226            }
227        }
228
229        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
230            let (server, server_name) =
231                IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
232            let url = format!("zed-cli://{server_name}");
233
234            match self {
235                Self::App { app_bundle, .. } => {
236                    let app_path = app_bundle;
237
238                    let status = unsafe {
239                        let app_url = CFURL::from_path(app_path, true)
240                            .with_context(|| format!("invalid app path {app_path:?}"))?;
241                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
242                            ptr::null(),
243                            url.as_ptr(),
244                            url.len() as CFIndex,
245                            kCFStringEncodingUTF8,
246                            ptr::null(),
247                        ));
248                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
249                        let urls_to_open =
250                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
251                        LSOpenFromURLSpec(
252                            &LSLaunchURLSpec {
253                                appURL: app_url.as_concrete_TypeRef(),
254                                itemURLs: urls_to_open.as_concrete_TypeRef(),
255                                passThruParams: ptr::null(),
256                                launchFlags: kLSLaunchDefaults,
257                                asyncRefCon: ptr::null_mut(),
258                            },
259                            ptr::null_mut(),
260                        )
261                    };
262
263                    anyhow::ensure!(
264                        status == 0,
265                        "cannot start app bundle {}",
266                        self.zed_version_string()
267                    );
268                }
269
270                Self::LocalPath { executable, .. } => {
271                    let executable_parent = executable
272                        .parent()
273                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
274                    let subprocess_stdout_file = fs::File::create(
275                        executable_parent.join("zed_dev.log"),
276                    )
277                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
278                    let subprocess_stdin_file =
279                        subprocess_stdout_file.try_clone().with_context(|| {
280                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
281                        })?;
282                    let mut command = std::process::Command::new(executable);
283                    let command = command
284                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
285                        .stderr(subprocess_stdout_file)
286                        .stdout(subprocess_stdin_file)
287                        .arg(url);
288
289                    command
290                        .spawn()
291                        .with_context(|| format!("Spawning {command:?}"))?;
292                }
293            }
294
295            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
296            Ok((handshake.requests, handshake.responses))
297        }
298
299        pub fn zed_version_string(&self) -> String {
300            let is_dev = matches!(self, Self::LocalPath { .. });
301            format!(
302                "Zed {}{}{}",
303                self.plist().bundle_short_version_string,
304                if is_dev { " (dev)" } else { "" },
305                self.path().display(),
306            )
307        }
308    }
309}