1use anyhow::{anyhow, Result};
2use clap::Parser;
3use cli::{CliRequest, CliResponse, IpcHandshake};
4use core_foundation::{
5 array::{CFArray, CFIndex},
6 string::kCFStringEncodingUTF8,
7 url::{CFURLCreateWithBytes, CFURL},
8};
9use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
10use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
11use serde::Deserialize;
12use std::{
13 ffi::OsStr,
14 fs::{self, OpenOptions},
15 io,
16 path::{Path, PathBuf},
17 ptr, thread,
18 time::Duration,
19};
20use sysinfo::{Pid, System, SystemExt};
21
22#[derive(Parser)]
23#[clap(name = "zed", global_setting(clap::AppSettings::NoAutoVersion))]
24struct Args {
25 /// Wait for all of the given paths to be closed before exiting.
26 #[clap(short, long)]
27 wait: bool,
28 /// A sequence of space-separated paths that you want to open.
29 #[clap()]
30 paths: Vec<PathBuf>,
31 /// Print Zed's version and the app path.
32 #[clap(short, long)]
33 version: bool,
34 /// Custom Zed.app path
35 #[clap(short, long)]
36 bundle_path: Option<PathBuf>,
37 #[clap(short, long)]
38 restart_from: Option<Pid>,
39}
40
41#[derive(Debug, Deserialize)]
42struct InfoPlist {
43 #[serde(rename = "CFBundleShortVersionString")]
44 bundle_short_version_string: String,
45}
46
47fn main() -> Result<()> {
48 let args = Args::parse();
49
50 let bundle_path = if let Some(bundle_path) = args.bundle_path {
51 bundle_path.canonicalize()?
52 } else {
53 locate_bundle()?
54 };
55
56 if args.version {
57 let plist_path = bundle_path.join("Contents/Info.plist");
58 let plist = plist::from_file::<_, InfoPlist>(plist_path)?;
59 println!(
60 "Zed {} – {}",
61 plist.bundle_short_version_string,
62 bundle_path.to_string_lossy()
63 );
64 return Ok(());
65 }
66
67 if let Some(parent_pid) = args.restart_from {
68 let mut system = System::new();
69 while system.refresh_process(parent_pid) {
70 thread::sleep(Duration::from_millis(100));
71 }
72 }
73
74 for path in args.paths.iter() {
75 if !path.exists() {
76 touch(path.as_path())?;
77 }
78 }
79
80 let (tx, rx) = launch_app(bundle_path)?;
81
82 tx.send(CliRequest::Open {
83 paths: args
84 .paths
85 .into_iter()
86 .map(|path| fs::canonicalize(path).map_err(|error| anyhow!(error)))
87 .collect::<Result<Vec<PathBuf>>>()?,
88 wait: args.wait,
89 })?;
90
91 while let Ok(response) = rx.recv() {
92 match response {
93 CliResponse::Ping => {}
94 CliResponse::Stdout { message } => println!("{message}"),
95 CliResponse::Stderr { message } => eprintln!("{message}"),
96 CliResponse::Exit { status } => std::process::exit(status),
97 }
98 }
99
100 Ok(())
101}
102
103fn touch(path: &Path) -> io::Result<()> {
104 match OpenOptions::new().create(true).write(true).open(path) {
105 Ok(_) => Ok(()),
106 Err(e) => Err(e),
107 }
108}
109
110fn locate_bundle() -> Result<PathBuf> {
111 let cli_path = std::env::current_exe()?.canonicalize()?;
112 let mut app_path = cli_path.clone();
113 while app_path.extension() != Some(OsStr::new("app")) {
114 if !app_path.pop() {
115 return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
116 }
117 }
118 Ok(app_path)
119}
120
121fn launch_app(app_path: PathBuf) -> Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
122 let (server, server_name) = IpcOneShotServer::<IpcHandshake>::new()?;
123 let url = format!("zed-cli://{server_name}");
124
125 let status = unsafe {
126 let app_url =
127 CFURL::from_path(&app_path, true).ok_or_else(|| anyhow!("invalid app path"))?;
128 let url_to_open = CFURL::wrap_under_create_rule(CFURLCreateWithBytes(
129 ptr::null(),
130 url.as_ptr(),
131 url.len() as CFIndex,
132 kCFStringEncodingUTF8,
133 ptr::null(),
134 ));
135 let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]);
136 LSOpenFromURLSpec(
137 &LSLaunchURLSpec {
138 appURL: app_url.as_concrete_TypeRef(),
139 itemURLs: urls_to_open.as_concrete_TypeRef(),
140 passThruParams: ptr::null(),
141 launchFlags: kLSLaunchDefaults,
142 asyncRefCon: ptr::null_mut(),
143 },
144 ptr::null_mut(),
145 )
146 };
147
148 if status == 0 {
149 let (_, handshake) = server.accept()?;
150 Ok((handshake.requests, handshake.responses))
151 } else {
152 Err(anyhow!("cannot start {:?}", app_path))
153 }
154}