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}