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// todo!("windows")
160#[cfg(target_os = "windows")]
161mod windows {
162    use std::path::Path;
163
164    use cli::{CliRequest, CliResponse};
165    use ipc_channel::ipc::{IpcReceiver, IpcSender};
166
167    use crate::{Bundle, InfoPlist};
168
169    impl Bundle {
170        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
171            unimplemented!()
172        }
173
174        pub fn plist(&self) -> &InfoPlist {
175            unimplemented!()
176        }
177
178        pub fn path(&self) -> &Path {
179            unimplemented!()
180        }
181
182        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
183            unimplemented!()
184        }
185
186        pub fn zed_version_string(&self) -> String {
187            unimplemented!()
188        }
189    }
190}
191
192#[cfg(target_os = "macos")]
193mod mac_os {
194    use anyhow::Context;
195    use core_foundation::{
196        array::{CFArray, CFIndex},
197        string::kCFStringEncodingUTF8,
198        url::{CFURLCreateWithBytes, CFURL},
199    };
200    use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
201    use std::{fs, path::Path, ptr};
202
203    use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
204    use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
205
206    use crate::{locate_bundle, Bundle, InfoPlist};
207
208    impl Bundle {
209        pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
210            let bundle_path = if let Some(bundle_path) = args_bundle_path {
211                bundle_path
212                    .canonicalize()
213                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
214            } else {
215                locate_bundle().context("bundle autodiscovery")?
216            };
217
218            match bundle_path.extension().and_then(|ext| ext.to_str()) {
219                Some("app") => {
220                    let plist_path = bundle_path.join("Contents/Info.plist");
221                    let plist =
222                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
223                            format!("Reading *.app bundle plist file at {plist_path:?}")
224                        })?;
225                    Ok(Self::App {
226                        app_bundle: bundle_path,
227                        plist,
228                    })
229                }
230                _ => {
231                    println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
232                    let plist_path = bundle_path
233                        .parent()
234                        .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
235                        .join("WebRTC.framework/Resources/Info.plist");
236                    let plist =
237                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
238                            format!("Reading dev bundle plist file at {plist_path:?}")
239                        })?;
240                    Ok(Self::LocalPath {
241                        executable: bundle_path,
242                        plist,
243                    })
244                }
245            }
246        }
247
248        fn plist(&self) -> &InfoPlist {
249            match self {
250                Self::App { plist, .. } => plist,
251                Self::LocalPath { plist, .. } => plist,
252            }
253        }
254
255        fn path(&self) -> &Path {
256            match self {
257                Self::App { app_bundle, .. } => app_bundle,
258                Self::LocalPath { executable, .. } => executable,
259            }
260        }
261
262        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
263            let (server, server_name) =
264                IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
265            let url = format!("zed-cli://{server_name}");
266
267            match self {
268                Self::App { app_bundle, .. } => {
269                    let app_path = app_bundle;
270
271                    let status = unsafe {
272                        let app_url = CFURL::from_path(app_path, true)
273                            .with_context(|| format!("invalid app path {app_path:?}"))?;
274                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
275                            ptr::null(),
276                            url.as_ptr(),
277                            url.len() as CFIndex,
278                            kCFStringEncodingUTF8,
279                            ptr::null(),
280                        ));
281                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
282                        let urls_to_open =
283                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
284                        LSOpenFromURLSpec(
285                            &LSLaunchURLSpec {
286                                appURL: app_url.as_concrete_TypeRef(),
287                                itemURLs: urls_to_open.as_concrete_TypeRef(),
288                                passThruParams: ptr::null(),
289                                launchFlags: kLSLaunchDefaults,
290                                asyncRefCon: ptr::null_mut(),
291                            },
292                            ptr::null_mut(),
293                        )
294                    };
295
296                    anyhow::ensure!(
297                        status == 0,
298                        "cannot start app bundle {}",
299                        self.zed_version_string()
300                    );
301                }
302
303                Self::LocalPath { executable, .. } => {
304                    let executable_parent = executable
305                        .parent()
306                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
307                    let subprocess_stdout_file = fs::File::create(
308                        executable_parent.join("zed_dev.log"),
309                    )
310                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
311                    let subprocess_stdin_file =
312                        subprocess_stdout_file.try_clone().with_context(|| {
313                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
314                        })?;
315                    let mut command = std::process::Command::new(executable);
316                    let command = command
317                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
318                        .stderr(subprocess_stdout_file)
319                        .stdout(subprocess_stdin_file)
320                        .arg(url);
321
322                    command
323                        .spawn()
324                        .with_context(|| format!("Spawning {command:?}"))?;
325                }
326            }
327
328            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
329            Ok((handshake.requests, handshake.responses))
330        }
331
332        pub fn zed_version_string(&self) -> String {
333            let is_dev = matches!(self, Self::LocalPath { .. });
334            format!(
335                "Zed {}{}{}",
336                self.plist().bundle_short_version_string,
337                if is_dev { " (dev)" } else { "" },
338                self.path().display(),
339            )
340        }
341    }
342}