main.rs

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