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}