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, Debug)]
 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    /// Add files to the currently open workspace
 22    #[clap(short, long, overrides_with = "new")]
 23    add: bool,
 24    /// Create a new workspace
 25    #[clap(short, long, overrides_with = "add")]
 26    new: 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    let open_new_workspace = if args.new {
 77        Some(true)
 78    } else if args.add {
 79        Some(false)
 80    } else {
 81        None
 82    };
 83
 84    tx.send(CliRequest::Open {
 85        paths: args
 86            .paths_with_position
 87            .into_iter()
 88            .map(|path_with_position| {
 89                let path_with_position = path_with_position.map_path_like(|path| {
 90                    fs::canonicalize(&path)
 91                        .with_context(|| format!("path {path:?} canonicalization"))
 92                })?;
 93                Ok(path_with_position.to_string(|path| path.display().to_string()))
 94            })
 95            .collect::<Result<_>>()?,
 96        wait: args.wait,
 97        open_new_workspace,
 98    })?;
 99
100    while let Ok(response) = rx.recv() {
101        match response {
102            CliResponse::Ping => {}
103            CliResponse::Stdout { message } => println!("{message}"),
104            CliResponse::Stderr { message } => eprintln!("{message}"),
105            CliResponse::Exit { status } => std::process::exit(status),
106        }
107    }
108
109    Ok(())
110}
111
112enum Bundle {
113    App {
114        app_bundle: PathBuf,
115        plist: InfoPlist,
116    },
117    LocalPath {
118        executable: PathBuf,
119        plist: InfoPlist,
120    },
121}
122
123fn touch(path: &Path) -> io::Result<()> {
124    match OpenOptions::new().create(true).write(true).open(path) {
125        Ok(_) => Ok(()),
126        Err(e) => Err(e),
127    }
128}
129
130fn locate_bundle() -> Result<PathBuf> {
131    let cli_path = std::env::current_exe()?.canonicalize()?;
132    let mut app_path = cli_path.clone();
133    while app_path.extension() != Some(OsStr::new("app")) {
134        if !app_path.pop() {
135            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
136        }
137    }
138    Ok(app_path)
139}
140
141#[cfg(target_os = "linux")]
142mod linux {
143    use std::path::Path;
144
145    use cli::{CliRequest, CliResponse};
146    use ipc_channel::ipc::{IpcReceiver, IpcSender};
147
148    use crate::{Bundle, InfoPlist};
149
150    impl Bundle {
151        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
152            unimplemented!()
153        }
154
155        pub fn plist(&self) -> &InfoPlist {
156            unimplemented!()
157        }
158
159        pub fn path(&self) -> &Path {
160            unimplemented!()
161        }
162
163        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
164            unimplemented!()
165        }
166
167        pub fn zed_version_string(&self) -> String {
168            unimplemented!()
169        }
170    }
171}
172
173// todo("windows")
174#[cfg(target_os = "windows")]
175mod windows {
176    use std::path::Path;
177
178    use cli::{CliRequest, CliResponse};
179    use ipc_channel::ipc::{IpcReceiver, IpcSender};
180
181    use crate::{Bundle, InfoPlist};
182
183    impl Bundle {
184        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
185            unimplemented!()
186        }
187
188        pub fn plist(&self) -> &InfoPlist {
189            unimplemented!()
190        }
191
192        pub fn path(&self) -> &Path {
193            unimplemented!()
194        }
195
196        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
197            unimplemented!()
198        }
199
200        pub fn zed_version_string(&self) -> String {
201            unimplemented!()
202        }
203    }
204}
205
206#[cfg(target_os = "macos")]
207mod mac_os {
208    use anyhow::Context;
209    use core_foundation::{
210        array::{CFArray, CFIndex},
211        string::kCFStringEncodingUTF8,
212        url::{CFURLCreateWithBytes, CFURL},
213    };
214    use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
215    use std::{fs, path::Path, ptr};
216
217    use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
218    use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
219
220    use crate::{locate_bundle, Bundle, InfoPlist};
221
222    impl Bundle {
223        pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
224            let bundle_path = if let Some(bundle_path) = args_bundle_path {
225                bundle_path
226                    .canonicalize()
227                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
228            } else {
229                locate_bundle().context("bundle autodiscovery")?
230            };
231
232            match bundle_path.extension().and_then(|ext| ext.to_str()) {
233                Some("app") => {
234                    let plist_path = bundle_path.join("Contents/Info.plist");
235                    let plist =
236                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
237                            format!("Reading *.app bundle plist file at {plist_path:?}")
238                        })?;
239                    Ok(Self::App {
240                        app_bundle: bundle_path,
241                        plist,
242                    })
243                }
244                _ => {
245                    println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
246                    let plist_path = bundle_path
247                        .parent()
248                        .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
249                        .join("WebRTC.framework/Resources/Info.plist");
250                    let plist =
251                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
252                            format!("Reading dev bundle plist file at {plist_path:?}")
253                        })?;
254                    Ok(Self::LocalPath {
255                        executable: bundle_path,
256                        plist,
257                    })
258                }
259            }
260        }
261
262        fn plist(&self) -> &InfoPlist {
263            match self {
264                Self::App { plist, .. } => plist,
265                Self::LocalPath { plist, .. } => plist,
266            }
267        }
268
269        fn path(&self) -> &Path {
270            match self {
271                Self::App { app_bundle, .. } => app_bundle,
272                Self::LocalPath { executable, .. } => executable,
273            }
274        }
275
276        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
277            let (server, server_name) =
278                IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
279            let url = format!("zed-cli://{server_name}");
280
281            match self {
282                Self::App { app_bundle, .. } => {
283                    let app_path = app_bundle;
284
285                    let status = unsafe {
286                        let app_url = CFURL::from_path(app_path, true)
287                            .with_context(|| format!("invalid app path {app_path:?}"))?;
288                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
289                            ptr::null(),
290                            url.as_ptr(),
291                            url.len() as CFIndex,
292                            kCFStringEncodingUTF8,
293                            ptr::null(),
294                        ));
295                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
296                        let urls_to_open =
297                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
298                        LSOpenFromURLSpec(
299                            &LSLaunchURLSpec {
300                                appURL: app_url.as_concrete_TypeRef(),
301                                itemURLs: urls_to_open.as_concrete_TypeRef(),
302                                passThruParams: ptr::null(),
303                                launchFlags: kLSLaunchDefaults,
304                                asyncRefCon: ptr::null_mut(),
305                            },
306                            ptr::null_mut(),
307                        )
308                    };
309
310                    anyhow::ensure!(
311                        status == 0,
312                        "cannot start app bundle {}",
313                        self.zed_version_string()
314                    );
315                }
316
317                Self::LocalPath { executable, .. } => {
318                    let executable_parent = executable
319                        .parent()
320                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
321                    let subprocess_stdout_file = fs::File::create(
322                        executable_parent.join("zed_dev.log"),
323                    )
324                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
325                    let subprocess_stdin_file =
326                        subprocess_stdout_file.try_clone().with_context(|| {
327                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
328                        })?;
329                    let mut command = std::process::Command::new(executable);
330                    let command = command
331                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
332                        .stderr(subprocess_stdout_file)
333                        .stdout(subprocess_stdin_file)
334                        .arg(url);
335
336                    command
337                        .spawn()
338                        .with_context(|| format!("Spawning {command:?}"))?;
339                }
340            }
341
342            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
343            Ok((handshake.requests, handshake.responses))
344        }
345
346        pub fn zed_version_string(&self) -> String {
347            let is_dev = matches!(self, Self::LocalPath { .. });
348            format!(
349                "Zed {}{}{}",
350                self.plist().bundle_short_version_string,
351                if is_dev { " (dev)" } else { "" },
352                self.path().display(),
353            )
354        }
355    }
356}