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