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,
 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    // Intercept version designators
 57    #[cfg(target_os = "macos")]
 58    if let Some(channel) = std::env::args().nth(1).filter(|arg| arg.starts_with("--")) {
 59        // When the first argument is a name of a release channel, we're gonna spawn off a cli of that version, with trailing args passed along.
 60        use std::str::FromStr as _;
 61
 62        if let Ok(channel) = release_channel::ReleaseChannel::from_str(&channel[2..]) {
 63            return mac_os::spawn_channel_cli(channel, std::env::args().skip(2).collect());
 64        }
 65    }
 66    let args = Args::parse();
 67
 68    let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
 69
 70    if args.version {
 71        println!("{}", bundle.zed_version_string());
 72        return Ok(());
 73    }
 74
 75    let curdir = env::current_dir()?;
 76    let mut paths = vec![];
 77    for path in args.paths_with_position {
 78        let canonicalized = path.map_path_like(|path| match fs::canonicalize(&path) {
 79            Ok(path) => Ok(path),
 80            Err(e) => {
 81                if let Some(mut parent) = path.parent() {
 82                    if parent == Path::new("") {
 83                        parent = &curdir;
 84                    }
 85                    match fs::canonicalize(parent) {
 86                        Ok(parent) => Ok(parent.join(path.file_name().unwrap())),
 87                        Err(_) => Err(e),
 88                    }
 89                } else {
 90                    Err(e)
 91                }
 92            }
 93        })?;
 94        paths.push(canonicalized.to_string(|path| path.display().to_string()))
 95    }
 96
 97    let (tx, rx) = bundle.launch()?;
 98    let open_new_workspace = if args.new {
 99        Some(true)
100    } else if args.add {
101        Some(false)
102    } else {
103        None
104    };
105
106    tx.send(CliRequest::Open {
107        paths,
108        wait: args.wait,
109        open_new_workspace,
110    })?;
111
112    while let Ok(response) = rx.recv() {
113        match response {
114            CliResponse::Ping => {}
115            CliResponse::Stdout { message } => println!("{message}"),
116            CliResponse::Stderr { message } => eprintln!("{message}"),
117            CliResponse::Exit { status } => std::process::exit(status),
118        }
119    }
120
121    Ok(())
122}
123
124enum Bundle {
125    App {
126        app_bundle: PathBuf,
127        plist: InfoPlist,
128    },
129    LocalPath {
130        executable: PathBuf,
131        plist: InfoPlist,
132    },
133}
134
135fn locate_bundle() -> Result<PathBuf> {
136    let cli_path = std::env::current_exe()?.canonicalize()?;
137    let mut app_path = cli_path.clone();
138    while app_path.extension() != Some(OsStr::new("app")) {
139        if !app_path.pop() {
140            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
141        }
142    }
143    Ok(app_path)
144}
145
146#[cfg(target_os = "linux")]
147mod linux {
148    use std::path::Path;
149
150    use cli::{CliRequest, CliResponse};
151    use ipc_channel::ipc::{IpcReceiver, IpcSender};
152
153    use crate::{Bundle, InfoPlist};
154
155    impl Bundle {
156        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
157            unimplemented!()
158        }
159
160        pub fn plist(&self) -> &InfoPlist {
161            unimplemented!()
162        }
163
164        pub fn path(&self) -> &Path {
165            unimplemented!()
166        }
167
168        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
169            unimplemented!()
170        }
171
172        pub fn zed_version_string(&self) -> String {
173            unimplemented!()
174        }
175    }
176}
177
178// todo("windows")
179#[cfg(target_os = "windows")]
180mod windows {
181    use std::path::Path;
182
183    use cli::{CliRequest, CliResponse};
184    use ipc_channel::ipc::{IpcReceiver, IpcSender};
185
186    use crate::{Bundle, InfoPlist};
187
188    impl Bundle {
189        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
190            unimplemented!()
191        }
192
193        pub fn plist(&self) -> &InfoPlist {
194            unimplemented!()
195        }
196
197        pub fn path(&self) -> &Path {
198            unimplemented!()
199        }
200
201        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
202            unimplemented!()
203        }
204
205        pub fn zed_version_string(&self) -> String {
206            unimplemented!()
207        }
208    }
209}
210
211#[cfg(target_os = "macos")]
212mod mac_os {
213    use anyhow::{Context, Result};
214    use core_foundation::{
215        array::{CFArray, CFIndex},
216        string::kCFStringEncodingUTF8,
217        url::{CFURLCreateWithBytes, CFURL},
218    };
219    use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
220    use std::{fs, path::Path, ptr};
221
222    use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
223    use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
224
225    use crate::{locate_bundle, Bundle, InfoPlist};
226
227    impl Bundle {
228        pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
229            let bundle_path = if let Some(bundle_path) = args_bundle_path {
230                bundle_path
231                    .canonicalize()
232                    .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
233            } else {
234                locate_bundle().context("bundle autodiscovery")?
235            };
236
237            match bundle_path.extension().and_then(|ext| ext.to_str()) {
238                Some("app") => {
239                    let plist_path = bundle_path.join("Contents/Info.plist");
240                    let plist =
241                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
242                            format!("Reading *.app bundle plist file at {plist_path:?}")
243                        })?;
244                    Ok(Self::App {
245                        app_bundle: bundle_path,
246                        plist,
247                    })
248                }
249                _ => {
250                    println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
251                    let plist_path = bundle_path
252                        .parent()
253                        .with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
254                        .join("WebRTC.framework/Resources/Info.plist");
255                    let plist =
256                        plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
257                            format!("Reading dev bundle plist file at {plist_path:?}")
258                        })?;
259                    Ok(Self::LocalPath {
260                        executable: bundle_path,
261                        plist,
262                    })
263                }
264            }
265        }
266
267        fn plist(&self) -> &InfoPlist {
268            match self {
269                Self::App { plist, .. } => plist,
270                Self::LocalPath { plist, .. } => plist,
271            }
272        }
273
274        fn path(&self) -> &Path {
275            match self {
276                Self::App { app_bundle, .. } => app_bundle,
277                Self::LocalPath { executable, .. } => executable,
278            }
279        }
280
281        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
282            let (server, server_name) =
283                IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
284            let url = format!("zed-cli://{server_name}");
285
286            match self {
287                Self::App { app_bundle, .. } => {
288                    let app_path = app_bundle;
289
290                    let status = unsafe {
291                        let app_url = CFURL::from_path(app_path, true)
292                            .with_context(|| format!("invalid app path {app_path:?}"))?;
293                        let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
294                            ptr::null(),
295                            url.as_ptr(),
296                            url.len() as CFIndex,
297                            kCFStringEncodingUTF8,
298                            ptr::null(),
299                        ));
300                        // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app
301                        let urls_to_open =
302                            CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
303                        LSOpenFromURLSpec(
304                            &LSLaunchURLSpec {
305                                appURL: app_url.as_concrete_TypeRef(),
306                                itemURLs: urls_to_open.as_concrete_TypeRef(),
307                                passThruParams: ptr::null(),
308                                launchFlags: kLSLaunchDefaults,
309                                asyncRefCon: ptr::null_mut(),
310                            },
311                            ptr::null_mut(),
312                        )
313                    };
314
315                    anyhow::ensure!(
316                        status == 0,
317                        "cannot start app bundle {}",
318                        self.zed_version_string()
319                    );
320                }
321
322                Self::LocalPath { executable, .. } => {
323                    let executable_parent = executable
324                        .parent()
325                        .with_context(|| format!("Executable {executable:?} path has no parent"))?;
326                    let subprocess_stdout_file = fs::File::create(
327                        executable_parent.join("zed_dev.log"),
328                    )
329                    .with_context(|| format!("Log file creation in {executable_parent:?}"))?;
330                    let subprocess_stdin_file =
331                        subprocess_stdout_file.try_clone().with_context(|| {
332                            format!("Cloning descriptor for file {subprocess_stdout_file:?}")
333                        })?;
334                    let mut command = std::process::Command::new(executable);
335                    let command = command
336                        .env(FORCE_CLI_MODE_ENV_VAR_NAME, "")
337                        .stderr(subprocess_stdout_file)
338                        .stdout(subprocess_stdin_file)
339                        .arg(url);
340
341                    command
342                        .spawn()
343                        .with_context(|| format!("Spawning {command:?}"))?;
344                }
345            }
346
347            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
348            Ok((handshake.requests, handshake.responses))
349        }
350
351        pub fn zed_version_string(&self) -> String {
352            let is_dev = matches!(self, Self::LocalPath { .. });
353            format!(
354                "Zed {}{}{}",
355                self.plist().bundle_short_version_string,
356                if is_dev { " (dev)" } else { "" },
357                self.path().display(),
358            )
359        }
360    }
361    pub(super) fn spawn_channel_cli(
362        channel: release_channel::ReleaseChannel,
363        leftover_args: Vec<String>,
364    ) -> Result<()> {
365        use anyhow::bail;
366        use std::process::Command;
367
368        let app_id_prompt = format!("id of app \"{}\"", channel.display_name());
369        let app_id_output = Command::new("osascript")
370            .arg("-e")
371            .arg(&app_id_prompt)
372            .output()?;
373        if !app_id_output.status.success() {
374            bail!("Could not determine app id for {}", channel.display_name());
375        }
376        let app_name = String::from_utf8(app_id_output.stdout)?.trim().to_owned();
377        let app_path_prompt = format!("kMDItemCFBundleIdentifier == '{app_name}'");
378        let app_path_output = Command::new("mdfind").arg(app_path_prompt).output()?;
379        if !app_path_output.status.success() {
380            bail!(
381                "Could not determine app path for {}",
382                channel.display_name()
383            );
384        }
385        let app_path = String::from_utf8(app_path_output.stdout)?.trim().to_owned();
386        let cli_path = format!("{app_path}/Contents/MacOS/cli");
387        Command::new(cli_path).args(leftover_args).spawn()?;
388        Ok(())
389    }
390}