linux cli (#11585)

Conrad Irwin created

- [x] Build out cli on linux
- [x] Add support for --dev-server-token sent by the CLI
- [x] Package cli into the .tar.gz
- [x] Link the cli to ~/.local/bin in install.sh

Release Notes:

- linux: Add cli support for managing zed

Change summary

Cargo.lock                              |  54 +++
Cargo.toml                              |   6 
crates/cli/Cargo.toml                   |   6 
crates/cli/src/cli.rs                   |   1 
crates/cli/src/main.rs                  | 339 ++++++++++++--------
crates/client/Cargo.toml                |   2 
crates/collab/src/tests/test_server.rs  |   6 
crates/extension/src/extension_store.rs |   2 
crates/fs/Cargo.toml                    |   2 
crates/headless/Cargo.toml              |   2 
crates/headless/src/headless.rs         |  64 +--
crates/release_channel/Cargo.toml       |   2 
crates/release_channel/src/lib.rs       |   3 
crates/terminal/Cargo.toml              |   2 
crates/zed/Cargo.toml                   |   4 
crates/zed/src/main.rs                  | 439 ++++++++++++--------------
crates/zed/src/zed.rs                   |   4 
crates/zed/src/zed/open_listener.rs     |  65 +++
script/bundle-linux                     |   4 
script/install.sh                       |   2 
20 files changed, 591 insertions(+), 418 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -2119,7 +2119,11 @@ dependencies = [
  "clap 4.4.4",
  "core-foundation",
  "core-services",
+ "exec",
+ "fork",
  "ipc-channel",
+ "libc",
+ "once_cell",
  "plist",
  "release_channel",
  "serde",
@@ -3575,6 +3579,17 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
 [[package]]
 name = "errno"
 version = "0.3.8"
@@ -3585,6 +3600,16 @@ dependencies = [
  "windows-sys 0.52.0",
 ]
 
+[[package]]
+name = "errno-dragonfly"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
+dependencies = [
+ "cc",
+ "libc",
+]
+
 [[package]]
 name = "etagere"
 version = "0.2.8"
@@ -3663,6 +3688,16 @@ dependencies = [
  "pin-project-lite",
 ]
 
+[[package]]
+name = "exec"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615"
+dependencies = [
+ "errno 0.2.8",
+ "libc",
+]
+
 [[package]]
 name = "extension"
 version = "0.1.0"
@@ -4063,6 +4098,15 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
 
+[[package]]
+name = "fork"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60e74d3423998a57e9d906e49252fb79eb4a04d5cdfe188fb1b7ff9fc076a8ed"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "form_urlencoded"
 version = "1.2.1"
@@ -4788,7 +4832,6 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "client",
- "ctrlc",
  "fs",
  "futures 0.3.28",
  "gpui",
@@ -4800,6 +4843,7 @@ dependencies = [
  "rpc",
  "settings",
  "shellexpand",
+ "signal-hook",
  "util",
 ]
 
@@ -8400,7 +8444,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06"
 dependencies = [
  "bitflags 1.3.2",
- "errno",
+ "errno 0.3.8",
  "io-lifetimes 1.0.11",
  "libc",
  "linux-raw-sys 0.3.8",
@@ -8414,7 +8458,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
 dependencies = [
  "bitflags 2.4.2",
- "errno",
+ "errno 0.3.8",
  "itoa",
  "libc",
  "linux-raw-sys 0.4.12",
@@ -8428,7 +8472,7 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a25c3aad9fc1424eb82c88087789a7d938e1829724f3e4043163baf0d13cfc12"
 dependencies = [
- "errno",
+ "errno 0.3.8",
  "libc",
  "rustix 0.38.32",
 ]
@@ -12832,7 +12876,6 @@ dependencies = [
  "clap 4.4.4",
  "cli",
  "client",
- "clock",
  "collab_ui",
  "collections",
  "command_palette",
@@ -12863,6 +12906,7 @@ dependencies = [
  "language_selector",
  "language_tools",
  "languages",
+ "libc",
  "log",
  "markdown_preview",
  "menu",

Cargo.toml 🔗

@@ -266,12 +266,14 @@ chrono = { version = "0.4", features = ["serde"] }
 clap = { version = "4.4", features = ["derive"] }
 clickhouse = { version = "0.11.6" }
 ctor = "0.2.6"
-ctrlc = "3.4.4"
+signal-hook = "0.3.17"
 core-foundation = { version = "0.9.3" }
 core-foundation-sys = "0.8.6"
 derive_more = "0.99.17"
 emojis = "0.6.1"
 env_logger = "0.9"
+exec = "0.3.1"
+fork = "0.1.23"
 futures = "0.3"
 futures-batch = "0.6.1"
 futures-lite = "1.13"
@@ -290,10 +292,12 @@ isahc = { version = "1.7.2", default-features = false, features = [
 ] }
 itertools = "0.11.0"
 lazy_static = "1.4.0"
+libc = "0.2"
 linkify = "0.10.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 nanoid = "0.4"
 nix = "0.28"
+once_cell = "1.19.0"
 ordered-float = "2.1.1"
 palette = { version = "0.7.5", default-features = false, features = ["std"] }
 parking_lot = "0.12.1"

crates/cli/Cargo.toml 🔗

@@ -19,11 +19,17 @@ path = "src/main.rs"
 [dependencies]
 anyhow.workspace = true
 clap.workspace = true
+libc.workspace = true
 ipc-channel = "0.18"
+once_cell.workspace = true
 release_channel.workspace = true
 serde.workspace = true
 util.workspace = true
 
+[target.'cfg(target_os = "linux")'.dependencies]
+exec.workspace =  true
+fork.workspace = true
+
 [target.'cfg(target_os = "macos")'.dependencies]
 core-foundation.workspace = true
 core-services = "0.2"

crates/cli/src/cli.rs 🔗

@@ -13,6 +13,7 @@ pub enum CliRequest {
         paths: Vec<String>,
         wait: bool,
         open_new_workspace: Option<bool>,
+        dev_server_token: Option<String>,
     },
 }
 

crates/cli/src/main.rs 🔗

@@ -1,17 +1,21 @@
 #![cfg_attr(any(target_os = "linux", target_os = "windows"), allow(dead_code))]
 
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
 use clap::Parser;
-use cli::{CliRequest, CliResponse};
-use serde::Deserialize;
+use cli::{ipc::IpcOneShotServer, CliRequest, CliResponse, IpcHandshake};
 use std::{
-    env,
-    ffi::OsStr,
-    fs,
+    env, fs,
     path::{Path, PathBuf},
 };
 use util::paths::PathLikeWithPosition;
 
+struct Detect;
+
+trait InstalledApp {
+    fn zed_version_string(&self) -> String;
+    fn launch(&self, ipc_url: String) -> anyhow::Result<()>;
+}
+
 #[derive(Parser, Debug)]
 #[command(name = "zed", disable_version_flag = true)]
 struct Args {
@@ -33,9 +37,9 @@ struct Args {
     /// Print Zed's version and the app path.
     #[arg(short, long)]
     version: bool,
-    /// Custom Zed.app path
-    #[arg(short, long)]
-    bundle_path: Option<PathBuf>,
+    /// Custom path to Zed.app or the zed binary
+    #[arg(long)]
+    zed: Option<PathBuf>,
     /// Run zed in dev-server mode
     #[arg(long)]
     dev_server_token: Option<String>,
@@ -49,12 +53,6 @@ fn parse_path_with_position(
     })
 }
 
-#[derive(Debug, Deserialize)]
-struct InfoPlist {
-    #[serde(rename = "CFBundleShortVersionString")]
-    bundle_short_version_string: String,
-}
-
 fn main() -> Result<()> {
     // Intercept version designators
     #[cfg(target_os = "macos")]
@@ -68,14 +66,10 @@ fn main() -> Result<()> {
     }
     let args = Args::parse();
 
-    let bundle = Bundle::detect(args.bundle_path.as_deref()).context("Bundle detection")?;
-
-    if let Some(dev_server_token) = args.dev_server_token {
-        return bundle.spawn(vec!["--dev-server-token".into(), dev_server_token]);
-    }
+    let app = Detect::detect(args.zed.as_deref()).context("Bundle detection")?;
 
     if args.version {
-        println!("{}", bundle.zed_version_string());
+        println!("{}", app.zed_version_string());
         return Ok(());
     }
 
@@ -101,7 +95,14 @@ fn main() -> Result<()> {
         paths.push(canonicalized.to_string(|path| path.display().to_string()))
     }
 
-    let (tx, rx) = bundle.launch()?;
+    let (server, server_name) =
+        IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
+    let url = format!("zed-cli://{server_name}");
+
+    app.launch(url)?;
+    let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
+    let (tx, rx) = (handshake.requests, handshake.responses);
+
     let open_new_workspace = if args.new {
         Some(true)
     } else if args.add {
@@ -114,6 +115,7 @@ fn main() -> Result<()> {
         paths,
         wait: args.wait,
         open_new_workspace,
+        dev_server_token: args.dev_server_token,
     })?;
 
     while let Ok(response) = rx.recv() {
@@ -128,60 +130,125 @@ fn main() -> Result<()> {
     Ok(())
 }
 
-enum Bundle {
-    App {
-        app_bundle: PathBuf,
-        plist: InfoPlist,
-    },
-    LocalPath {
-        executable: PathBuf,
-        plist: InfoPlist,
-    },
-}
-
-fn locate_bundle() -> Result<PathBuf> {
-    let cli_path = std::env::current_exe()?.canonicalize()?;
-    let mut app_path = cli_path.clone();
-    while app_path.extension() != Some(OsStr::new("app")) {
-        if !app_path.pop() {
-            return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
-        }
-    }
-    Ok(app_path)
-}
-
 #[cfg(target_os = "linux")]
 mod linux {
-    use std::path::Path;
+    use std::{
+        env,
+        ffi::OsString,
+        io,
+        os::{
+            linux::net::SocketAddrExt,
+            unix::net::{SocketAddr, UnixDatagram},
+        },
+        path::{Path, PathBuf},
+        process, thread,
+        time::Duration,
+    };
 
-    use cli::{CliRequest, CliResponse};
-    use ipc_channel::ipc::{IpcReceiver, IpcSender};
+    use anyhow::anyhow;
+    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
+    use fork::Fork;
+    use once_cell::sync::Lazy;
 
-    use crate::{Bundle, InfoPlist};
+    use crate::{Detect, InstalledApp};
 
-    impl Bundle {
-        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
-            unimplemented!()
-        }
+    static RELEASE_CHANNEL: Lazy<String> =
+        Lazy::new(|| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string());
 
-        pub fn plist(&self) -> &InfoPlist {
-            unimplemented!()
+    struct App(PathBuf);
+
+    impl Detect {
+        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
+            let path = if let Some(path) = path {
+                path.to_path_buf().canonicalize()
+            } else {
+                let cli = env::current_exe()?;
+                let dir = cli
+                    .parent()
+                    .ok_or_else(|| anyhow!("no parent path for cli"))?;
+
+                match dir.join("zed").canonicalize() {
+                    Ok(path) => Ok(path),
+                    // development builds have Zed capitalized
+                    Err(e) => match dir.join("Zed").canonicalize() {
+                        Ok(path) => Ok(path),
+                        Err(_) => Err(e),
+                    },
+                }
+            }?;
+
+            Ok(App(path))
         }
+    }
 
-        pub fn path(&self) -> &Path {
-            unimplemented!()
+    impl InstalledApp for App {
+        fn zed_version_string(&self) -> String {
+            format!(
+                "Zed {}{} – {}",
+                if *RELEASE_CHANNEL == "stable" {
+                    "".to_string()
+                } else {
+                    format!(" {} ", *RELEASE_CHANNEL)
+                },
+                option_env!("RELEASE_VERSION").unwrap_or_default(),
+                self.0.display(),
+            )
         }
 
-        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
-            unimplemented!()
+        fn launch(&self, ipc_url: String) -> anyhow::Result<()> {
+            let uid: u32 = unsafe { libc::getuid() };
+            let sock_addr =
+                SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL, uid))?;
+
+            let sock = UnixDatagram::unbound()?;
+            if sock.connect_addr(&sock_addr).is_err() {
+                self.boot_background(ipc_url)?;
+            } else {
+                sock.send(ipc_url.as_bytes())?;
+            }
+            Ok(())
         }
+    }
 
-        pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
-            unimplemented!()
+    impl App {
+        fn boot_background(&self, ipc_url: String) -> anyhow::Result<()> {
+            let path = &self.0;
+
+            match fork::fork() {
+                Ok(Fork::Parent(_)) => Ok(()),
+                Ok(Fork::Child) => {
+                    std::env::set_var(FORCE_CLI_MODE_ENV_VAR_NAME, "");
+                    if let Err(_) = fork::setsid() {
+                        eprintln!("failed to setsid: {}", std::io::Error::last_os_error());
+                        process::exit(1);
+                    }
+                    if std::env::var("ZED_KEEP_FD").is_err() {
+                        if let Err(_) = fork::close_fd() {
+                            eprintln!("failed to close_fd: {}", std::io::Error::last_os_error());
+                        }
+                    }
+                    let error =
+                        exec::execvp(path.clone(), &[path.as_os_str(), &OsString::from(ipc_url)]);
+                    // if exec succeeded, we never get here.
+                    eprintln!("failed to exec {:?}: {}", path, error);
+                    process::exit(1)
+                }
+                Err(_) => Err(anyhow!(io::Error::last_os_error())),
+            }
         }
 
-        pub fn zed_version_string(&self) -> String {
-            unimplemented!()
+        fn wait_for_socket(
+            &self,
+            sock_addr: &SocketAddr,
+            sock: &mut UnixDatagram,
+        ) -> Result<(), std::io::Error> {
+            for _ in 0..100 {
+                thread::sleep(Duration::from_millis(10));
+                if sock.connect_addr(&sock_addr).is_ok() {
+                    return Ok(());
+                }
+            }
+            sock.connect_addr(&sock_addr)
         }
     }
 }
@@ -189,59 +256,79 @@ mod linux {
 // todo("windows")
 #[cfg(target_os = "windows")]
 mod windows {
+    use crate::{Detect, InstalledApp};
     use std::path::Path;
 
-    use cli::{CliRequest, CliResponse};
-    use ipc_channel::ipc::{IpcReceiver, IpcSender};
-
-    use crate::{Bundle, InfoPlist};
-
-    impl Bundle {
-        pub fn detect(_args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
-            unimplemented!()
-        }
-
-        pub fn plist(&self) -> &InfoPlist {
-            unimplemented!()
-        }
-
-        pub fn path(&self) -> &Path {
-            unimplemented!()
-        }
-
-        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
+    struct App;
+    impl InstalledApp for App {
+        fn zed_version_string(&self) -> String {
             unimplemented!()
         }
-
-        pub fn spawn(&self, _args: Vec<String>) -> anyhow::Result<()> {
+        fn launch(&self, _ipc_url: String) -> anyhow::Result<()> {
             unimplemented!()
         }
+    }
 
-        pub fn zed_version_string(&self) -> String {
-            unimplemented!()
+    impl Detect {
+        pub fn detect(_path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
+            Ok(App)
         }
     }
 }
 
 #[cfg(target_os = "macos")]
 mod mac_os {
-    use anyhow::{Context, Result};
+    use anyhow::{anyhow, Context, Result};
     use core_foundation::{
         array::{CFArray, CFIndex},
         string::kCFStringEncodingUTF8,
         url::{CFURLCreateWithBytes, CFURL},
     };
     use core_services::{kLSLaunchDefaults, LSLaunchURLSpec, LSOpenFromURLSpec, TCFType};
-    use std::{fs, path::Path, process::Command, ptr};
+    use serde::Deserialize;
+    use std::{
+        ffi::OsStr,
+        fs,
+        path::{Path, PathBuf},
+        process::Command,
+        ptr,
+    };
 
-    use cli::{CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME};
-    use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
+    use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
 
-    use crate::{locate_bundle, Bundle, InfoPlist};
+    use crate::{Detect, InstalledApp};
 
-    impl Bundle {
-        pub fn detect(args_bundle_path: Option<&Path>) -> anyhow::Result<Self> {
-            let bundle_path = if let Some(bundle_path) = args_bundle_path {
+    #[derive(Debug, Deserialize)]
+    struct InfoPlist {
+        #[serde(rename = "CFBundleShortVersionString")]
+        bundle_short_version_string: String,
+    }
+
+    enum Bundle {
+        App {
+            app_bundle: PathBuf,
+            plist: InfoPlist,
+        },
+        LocalPath {
+            executable: PathBuf,
+            plist: InfoPlist,
+        },
+    }
+
+    fn locate_bundle() -> Result<PathBuf> {
+        let cli_path = std::env::current_exe()?.canonicalize()?;
+        let mut app_path = cli_path.clone();
+        while app_path.extension() != Some(OsStr::new("app")) {
+            if !app_path.pop() {
+                return Err(anyhow!("cannot find app bundle containing {:?}", cli_path));
+            }
+        }
+        Ok(app_path)
+    }
+
+    impl Detect {
+        pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
+            let bundle_path = if let Some(bundle_path) = path {
                 bundle_path
                     .canonicalize()
                     .with_context(|| format!("Args bundle path {bundle_path:?} canonicalization"))?
@@ -256,7 +343,7 @@ mod mac_os {
                         plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
                             format!("Reading *.app bundle plist file at {plist_path:?}")
                         })?;
-                    Ok(Self::App {
+                    Ok(Bundle::App {
                         app_bundle: bundle_path,
                         plist,
                     })
@@ -271,42 +358,27 @@ mod mac_os {
                         plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
                             format!("Reading dev bundle plist file at {plist_path:?}")
                         })?;
-                    Ok(Self::LocalPath {
+                    Ok(Bundle::LocalPath {
                         executable: bundle_path,
                         plist,
                     })
                 }
             }
         }
+    }
 
-        fn plist(&self) -> &InfoPlist {
-            match self {
-                Self::App { plist, .. } => plist,
-                Self::LocalPath { plist, .. } => plist,
-            }
-        }
-
-        fn path(&self) -> &Path {
-            match self {
-                Self::App { app_bundle, .. } => app_bundle,
-                Self::LocalPath { executable, .. } => executable,
-            }
-        }
-
-        pub fn spawn(&self, args: Vec<String>) -> Result<()> {
-            let path = match self {
-                Self::App { app_bundle, .. } => app_bundle.join("Contents/MacOS/zed"),
-                Self::LocalPath { executable, .. } => executable.clone(),
-            };
-            Command::new(path).args(args).status()?;
-            Ok(())
+    impl InstalledApp for Bundle {
+        fn zed_version_string(&self) -> String {
+            let is_dev = matches!(self, Self::LocalPath { .. });
+            format!(
+                "Zed {}{} – {}",
+                self.plist().bundle_short_version_string,
+                if is_dev { " (dev)" } else { "" },
+                self.path().display(),
+            )
         }
 
-        pub fn launch(&self) -> anyhow::Result<(IpcSender<CliRequest>, IpcReceiver<CliResponse>)> {
-            let (server, server_name) =
-                IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
-            let url = format!("zed-cli://{server_name}");
-
+        fn launch(&self, url: String) -> anyhow::Result<()> {
             match self {
                 Self::App { app_bundle, .. } => {
                     let app_path = app_bundle;
@@ -368,18 +440,23 @@ mod mac_os {
                 }
             }
 
-            let (_, handshake) = server.accept().context("Handshake after Zed spawn")?;
-            Ok((handshake.requests, handshake.responses))
+            Ok(())
         }
+    }
 
-        pub fn zed_version_string(&self) -> String {
-            let is_dev = matches!(self, Self::LocalPath { .. });
-            format!(
-                "Zed {}{} – {}",
-                self.plist().bundle_short_version_string,
-                if is_dev { " (dev)" } else { "" },
-                self.path().display(),
-            )
+    impl Bundle {
+        fn plist(&self) -> &InfoPlist {
+            match self {
+                Self::App { plist, .. } => plist,
+                Self::LocalPath { plist, .. } => plist,
+            }
+        }
+
+        fn path(&self) -> &Path {
+            match self {
+                Self::App { app_bundle, .. } => app_bundle,
+                Self::LocalPath { executable, .. } => executable,
+            }
         }
     }
 

crates/client/Cargo.toml 🔗

@@ -27,7 +27,7 @@ futures.workspace = true
 gpui.workspace = true
 lazy_static.workspace = true
 log.workspace = true
-once_cell = "1.19.0"
+once_cell.workspace = true
 parking_lot.workspace = true
 postage.workspace = true
 rand.workspace = true

crates/extension/src/extension_store.rs 🔗

@@ -164,7 +164,7 @@ pub struct ExtensionIndexLanguageEntry {
 actions!(zed, [ReloadExtensions]);
 
 pub fn init(
-    fs: Arc<fs::RealFs>,
+    fs: Arc<dyn Fs>,
     client: Arc<Client>,
     node_runtime: Arc<dyn NodeRuntime>,
     language_registry: Arc<LanguageRegistry>,

crates/fs/Cargo.toml 🔗

@@ -29,7 +29,7 @@ git.workspace = true
 git2.workspace = true
 serde.workspace = true
 serde_json.workspace = true
-libc = "0.2"
+libc.workspace = true
 time.workspace = true
 
 gpui = { workspace = true, optional = true }

crates/headless/Cargo.toml 🔗

@@ -15,7 +15,7 @@ doctest = false
 [dependencies]
 anyhow.workspace = true
 client.workspace = true
-ctrlc.workspace = true
+signal-hook.workspace = true
 gpui.workspace = true
 log.workspace = true
 rpc.workspace = true

crates/headless/src/headless.rs 🔗

@@ -1,4 +1,4 @@
-use anyhow::Result;
+use anyhow::{anyhow, Result};
 use client::DevServerProjectId;
 use client::{user::UserStore, Client, ClientSettings};
 use fs::Fs;
@@ -36,7 +36,7 @@ struct GlobalDevServer(Model<DevServer>);
 
 impl Global for GlobalDevServer {}
 
-pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
+pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) -> Task<Result<()>> {
     let dev_server = cx.new_model(|cx| DevServer::new(client.clone(), app_state, cx));
     cx.set_global(GlobalDevServer(dev_server.clone()));
 
@@ -49,42 +49,36 @@ pub fn init(client: Arc<Client>, app_state: AppState, cx: &mut AppContext) {
         });
     });
 
-    // Set up a handler when the dev server is shut down by the user pressing Ctrl-C
-    let (tx, rx) = futures::channel::oneshot::channel();
-    set_ctrlc_handler(move || tx.send(()).log_err().unwrap()).log_err();
-
-    cx.spawn(|cx| async move {
-        rx.await.log_err();
-        log::info!("Received interrupt signal");
-        cx.update(|cx| cx.quit()).log_err();
-    })
-    .detach();
-
-    let server_url = ClientSettings::get_global(&cx).server_url.clone();
-    cx.spawn(|cx| async move {
-        match client.authenticate_and_connect(false, &cx).await {
-            Ok(_) => {
-                log::info!("Connected to {}", server_url);
+    #[cfg(not(target_os = "windows"))]
+    {
+        use signal_hook::consts::{SIGINT, SIGTERM};
+        use signal_hook::iterator::Signals;
+        // Set up a handler when the dev server is shut down
+        // with ctrl-c or kill
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let mut signals = Signals::new(&[SIGTERM, SIGINT]).unwrap();
+        std::thread::spawn({
+            move || {
+                if let Some(sig) = signals.forever().next() {
+                    tx.send(sig).log_err();
+                }
             }
-            Err(e) => {
-                log::error!("Error connecting to '{}': {}", server_url, e);
+        });
+        cx.spawn(|cx| async move {
+            if let Ok(sig) = rx.await {
+                log::info!("received signal {sig:?}");
                 cx.update(|cx| cx.quit()).log_err();
             }
-        }
-    })
-    .detach();
-}
+        })
+        .detach();
+    }
 
-fn set_ctrlc_handler<F>(f: F) -> Result<(), ctrlc::Error>
-where
-    F: FnOnce() + 'static + Send,
-{
-    let f = std::sync::Mutex::new(Some(f));
-    ctrlc::set_handler(move || {
-        if let Ok(mut guard) = f.lock() {
-            let f = guard.take().expect("f can only be taken once");
-            f();
-        }
+    let server_url = ClientSettings::get_global(&cx).server_url.clone();
+    cx.spawn(|cx| async move {
+        client
+            .authenticate_and_connect(false, &cx)
+            .await
+            .map_err(|e| anyhow!("Error connecting to '{}': {}", server_url, e))
     })
 }
 
@@ -186,7 +180,7 @@ impl DevServer {
 
         let path_exists = fs.is_dir(path).await;
         if !path_exists {
-            return Err(anyhow::anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
+            return Err(anyhow!(ErrorCode::DevServerProjectPathDoesNotExist))?;
         }
 
         Ok(proto::Ack {})

crates/release_channel/src/lib.rs 🔗

@@ -7,7 +7,8 @@ use std::{env, str::FromStr};
 use gpui::{AppContext, Global, SemanticVersion};
 use once_cell::sync::Lazy;
 
-static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
+/// stable | dev | nightly | preview
+pub static RELEASE_CHANNEL_NAME: Lazy<String> = if cfg!(debug_assertions) {
     Lazy::new(|| {
         env::var("ZED_RELEASE_CHANNEL")
             .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string())

crates/terminal/Cargo.toml 🔗

@@ -20,7 +20,7 @@ collections.workspace = true
 dirs = "4.0.0"
 futures.workspace = true
 gpui.workspace = true
-libc = "0.2"
+libc.workspace = true
 task.workspace = true
 schemars.workspace = true
 serde.workspace = true

crates/zed/Cargo.toml 🔗

@@ -30,7 +30,6 @@ chrono.workspace = true
 clap.workspace = true
 cli.workspace = true
 client.workspace = true
-clock.workspace = true
 collab_ui.workspace = true
 collections.workspace = true
 command_palette.workspace = true
@@ -60,11 +59,12 @@ language.workspace = true
 language_selector.workspace = true
 language_tools.workspace = true
 languages.workspace = true
+libc.workspace = true
 log.workspace = true
 markdown_preview.workspace = true
 menu.workspace = true
 mimalloc = { version = "0.1", optional = true }
-nix = {workspace = true, features = ["pthread"] }
+nix = {workspace = true, features = ["pthread", "signal"] }
 node_runtime.workspace = true
 notifications.workspace = true
 outline.workspace = true

crates/zed/src/main.rs 🔗

@@ -17,7 +17,7 @@ use env_logger::Builder;
 use fs::RealFs;
 use futures::{future, StreamExt};
 use git::GitHostingProviderRegistry;
-use gpui::{App, AppContext, AsyncAppContext, Context, Task, VisualContext};
+use gpui::{App, AppContext, AsyncAppContext, Context, Global, Task, VisualContext};
 use image_viewer;
 use language::LanguageRegistry;
 use log::LevelFilter;
@@ -26,9 +26,7 @@ use assets::Assets;
 use node_runtime::RealNodeRuntime;
 use parking_lot::Mutex;
 use release_channel::AppCommitSha;
-use settings::{
-    default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore,
-};
+use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore};
 use simplelog::ConfigBuilder;
 use smol::process::Command;
 use std::{
@@ -36,11 +34,11 @@ use std::{
     fs::OpenOptions,
     io::{IsTerminal, Write},
     path::Path,
+    process,
     sync::Arc,
 };
 use theme::{ActiveTheme, SystemAppearance, ThemeRegistry, ThemeSettings};
 use util::{
-    http::HttpClientWithUrl,
     maybe, parse_env_output,
     paths::{self},
     ResultExt, TryFutureExt,
@@ -49,9 +47,8 @@ use uuid::Uuid;
 use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
 use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
 use zed::{
-    app_menus, build_window_options, ensure_only_instance, handle_cli_connection,
-    handle_keymap_file_changes, initialize_workspace, open_paths_with_positions, IsOnlyInstance,
-    OpenListener, OpenRequest,
+    app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
+    initialize_workspace, open_paths_with_positions, OpenListener, OpenRequest,
 };
 
 use crate::zed::inline_completion_registry;
@@ -76,95 +73,169 @@ fn fail_to_launch(e: anyhow::Error) {
     })
 }
 
-fn init_headless(dev_server_token: DevServerToken) {
-    if let Err(e) = init_paths() {
-        log::error!("Failed to launch: {}", e);
-        return;
-    }
-    init_logger();
-
-    let app = App::new();
-
-    let session_id = Uuid::new_v4().to_string();
-    let (installation_id, _) = app
-        .background_executor()
-        .block(installation_id())
-        .ok()
-        .unzip();
-
-    reliability::init_panic_hook(&app, installation_id.clone(), session_id.clone());
+enum AppMode {
+    Headless(DevServerToken),
+    Ui,
+}
+impl Global for AppMode {}
 
-    app.run(|cx| {
-        release_channel::init(env!("CARGO_PKG_VERSION"), cx);
-        if let Some(build_sha) = option_env!("ZED_COMMIT_SHA") {
-            AppCommitSha::set_global(AppCommitSha(build_sha.into()), cx);
+fn init_headless(
+    dev_server_token: DevServerToken,
+    app_state: Arc<AppState>,
+    cx: &mut AppContext,
+) -> Task<Result<()>> {
+    match cx.try_global::<AppMode>() {
+        Some(AppMode::Headless(token)) if token == &dev_server_token => return Task::ready(Ok(())),
+        Some(_) => {
+            return Task::ready(Err(anyhow!(
+                "zed is already running. Use `kill {}` to stop it",
+                process::id()
+            )))
         }
+        None => {
+            cx.set_global(AppMode::Headless(dev_server_token.clone()));
+        }
+    };
+    let client = app_state.client.clone();
+    client.set_dev_server_token(dev_server_token);
+    headless::init(
+        client.clone(),
+        headless::AppState {
+            languages: app_state.languages.clone(),
+            user_store: app_state.user_store.clone(),
+            fs: app_state.fs.clone(),
+            node_runtime: app_state.node_runtime.clone(),
+        },
+        cx,
+    )
+}
 
-        let mut store = SettingsStore::default();
-        store
-            .set_default_settings(default_settings().as_ref(), cx)
-            .unwrap();
-        cx.set_global(store);
-
-        client::init_settings(cx);
-
-        let clock = Arc::new(clock::RealSystemClock);
-        let http = Arc::new(HttpClientWithUrl::new(
-            &client::ClientSettings::get_global(cx).server_url,
-        ));
-
-        let client = client::Client::new(clock, http.clone(), cx);
-        let client = client.clone();
-        client.set_dev_server_token(dev_server_token);
-
-        project::Project::init(&client, cx);
-        client::init(&client, cx);
+fn init_ui(app_state: Arc<AppState>, cx: &mut AppContext) -> Result<()> {
+    match cx.try_global::<AppMode>() {
+        Some(AppMode::Headless(_)) => {
+            return Err(anyhow!(
+                "zed is already running in headless mode. Use `kill {}` to stop it",
+                process::id()
+            ))
+        }
+        Some(AppMode::Ui) => return Ok(()),
+        None => {
+            cx.set_global(AppMode::Ui);
+        }
+    };
 
-        let git_hosting_provider_registry = GitHostingProviderRegistry::default_global(cx);
-        let git_binary_path = if option_env!("ZED_BUNDLE").as_deref() == Some("true") {
-            cx.path_for_auxiliary_executable("git")
-                .context("could not find git binary path")
-                .log_err()
-        } else {
-            None
-        };
-        let fs = Arc::new(RealFs::new(git_hosting_provider_registry, git_binary_path));
+    SystemAppearance::init(cx);
+    load_embedded_fonts(cx);
+
+    theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
+    app_state.languages.set_theme(cx.theme().clone());
+    command_palette::init(cx);
+    editor::init(cx);
+    image_viewer::init(cx);
+    diagnostics::init(cx);
+
+    audio::init(Assets, cx);
+    workspace::init(app_state.clone(), cx);
+    recent_projects::init(cx);
+
+    go_to_line::init(cx);
+    file_finder::init(cx);
+    tab_switcher::init(cx);
+    outline::init(cx);
+    project_symbols::init(cx);
+    project_panel::init(Assets, cx);
+    tasks_ui::init(cx);
+    channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
+    search::init(cx);
+    vim::init(cx);
+    terminal_view::init(cx);
+
+    journal::init(app_state.clone(), cx);
+    language_selector::init(cx);
+    theme_selector::init(cx);
+    language_tools::init(cx);
+    call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+    notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+    collab_ui::init(&app_state, cx);
+    feedback::init(cx);
+    markdown_preview::init(cx);
+    welcome::init(cx);
+    extensions_ui::init(cx);
+
+    // Initialize each completion provider. Settings are used for toggling between them.
+    let copilot_language_server_id = app_state.languages.next_language_server_id();
+    copilot::init(
+        copilot_language_server_id,
+        app_state.client.http_client(),
+        app_state.node_runtime.clone(),
+        cx,
+    );
+    supermaven::init(app_state.client.clone(), cx);
+
+    inline_completion_registry::init(app_state.client.telemetry().clone(), cx);
+
+    assistant::init(app_state.client.clone(), cx);
+    assistant2::init(app_state.client.clone(), cx);
+
+    cx.observe_global::<SettingsStore>({
+        let languages = app_state.languages.clone();
+        let http = app_state.client.http_client();
+        let client = app_state.client.clone();
+
+        move |cx| {
+            for &mut window in cx.windows().iter_mut() {
+                let background_appearance = cx.theme().window_background_appearance();
+                window
+                    .update(cx, |_, cx| {
+                        cx.set_background_appearance(background_appearance)
+                    })
+                    .ok();
+            }
+            languages.set_theme(cx.theme().clone());
+            let new_host = &client::ClientSettings::get_global(cx).server_url;
+            if &http.base_url() != new_host {
+                http.set_base_url(new_host);
+                if client.status().borrow().is_connected() {
+                    client.reconnect(&cx.to_async());
+                }
+            }
+        }
+    })
+    .detach();
+    let telemetry = app_state.client.telemetry();
+    telemetry.report_setting_event("theme", cx.theme().name.to_string());
+    telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
+    telemetry.flush_events();
+
+    extension::init(
+        app_state.fs.clone(),
+        app_state.client.clone(),
+        app_state.node_runtime.clone(),
+        app_state.languages.clone(),
+        ThemeRegistry::global(cx),
+        cx,
+    );
 
-        git_hosting_providers::init(cx);
+    dev_server_projects::init(app_state.client.clone(), cx);
 
-        let mut languages =
-            LanguageRegistry::new(Task::ready(()), cx.background_executor().clone());
-        languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
-        let languages = Arc::new(languages);
-        let node_runtime = RealNodeRuntime::new(http.clone());
+    let fs = app_state.fs.clone();
+    load_user_themes_in_background(fs.clone(), cx);
+    watch_themes(fs.clone(), cx);
+    watch_languages(fs.clone(), app_state.languages.clone(), cx);
+    watch_file_types(fs.clone(), cx);
 
-        language::init(cx);
-        languages::init(languages.clone(), node_runtime.clone(), cx);
-        let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
+    cx.set_menus(app_menus());
+    initialize_workspace(app_state.clone(), cx);
 
-        let user_settings_file_rx = watch_config_file(
-            &cx.background_executor(),
-            fs.clone(),
-            paths::SETTINGS.clone(),
-        );
-        handle_settings_file_changes(user_settings_file_rx, cx);
+    cx.activate(true);
 
-        reliability::init(client.http_client(), installation_id, cx);
+    cx.spawn(|cx| async move { authenticate(app_state.client.clone(), &cx).await })
+        .detach_and_log_err(cx);
 
-        headless::init(
-            client.clone(),
-            headless::AppState {
-                languages: languages.clone(),
-                user_store: user_store.clone(),
-                fs: fs.clone(),
-                node_runtime: node_runtime.clone(),
-            },
-            cx,
-        );
-    })
+    Ok(())
 }
 
-fn init_ui(args: Args) {
+fn main() {
     menu::init();
     zed_actions::init();
 
@@ -175,10 +246,6 @@ fn init_ui(args: Args) {
 
     init_logger();
 
-    if ensure_only_instance() != IsOnlyInstance::Yes {
-        return;
-    }
-
     log::info!("========== starting zed ==========");
     let app = App::new().with_assets(Assets);
 
@@ -190,6 +257,26 @@ fn init_ui(args: Args) {
     let session_id = Uuid::new_v4().to_string();
     reliability::init_panic_hook(&app, installation_id.clone(), session_id.clone());
 
+    let (listener, mut open_rx) = OpenListener::new();
+    let listener = Arc::new(listener);
+    let open_listener = listener.clone();
+
+    #[cfg(target_os = "linux")]
+    {
+        if crate::zed::listen_for_cli_connections(listener.clone()).is_err() {
+            println!("zed is already running");
+            return;
+        }
+    }
+    #[cfg(not(target_os = "linux"))]
+    {
+        use zed::only_instance::*;
+        if ensure_only_instance() != IsOnlyInstance::Yes {
+            println!("zed is already running");
+            return;
+        }
+    }
+
     let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new());
     let git_binary_path = if option_env!("ZED_BUNDLE").as_deref() == Some("true") {
         app.path_for_auxiliary_executable("git")
@@ -223,9 +310,6 @@ fn init_ui(args: Args) {
         })
     };
 
-    let (listener, mut open_rx) = OpenListener::new();
-    let listener = Arc::new(listener);
-    let open_listener = listener.clone();
     app.on_open_urls(move |urls| open_listener.open_urls(urls));
     app.on_reopen(move |cx| {
         if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade())
@@ -247,11 +331,8 @@ fn init_ui(args: Args) {
         GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
         git_hosting_providers::init(cx);
 
-        SystemAppearance::init(cx);
         OpenListener::set_global(listener.clone(), cx);
 
-        load_embedded_fonts(cx);
-
         settings::init(cx);
         handle_settings_file_changes(user_settings_file_rx, cx);
         handle_keymap_file_changes(user_keymap_file_rx, cx);
@@ -260,7 +341,6 @@ fn init_ui(args: Args) {
         let client = Client::production(cx);
         let mut languages =
             LanguageRegistry::new(login_shell_env_loaded, cx.background_executor().clone());
-        let copilot_language_server_id = languages.next_language_server_id();
         languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
         let languages = Arc::new(languages);
         let node_runtime = RealNodeRuntime::new(client.http_client());
@@ -273,76 +353,11 @@ fn init_ui(args: Args) {
         Client::set_global(client.clone(), cx);
 
         zed::init(cx);
-        theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
         project::Project::init(&client, cx);
         client::init(&client, cx);
-        command_palette::init(cx);
         language::init(cx);
-        editor::init(cx);
-        image_viewer::init(cx);
-        diagnostics::init(cx);
-
-        // Initialize each completion provider. Settings are used for toggling between them.
-        copilot::init(
-            copilot_language_server_id,
-            client.http_client(),
-            node_runtime.clone(),
-            cx,
-        );
-        supermaven::init(client.clone(), cx);
-
-        assistant::init(client.clone(), cx);
-        assistant2::init(client.clone(), cx);
-
-        inline_completion_registry::init(client.telemetry().clone(), cx);
-
-        extension::init(
-            fs.clone(),
-            client.clone(),
-            node_runtime.clone(),
-            languages.clone(),
-            ThemeRegistry::global(cx),
-            cx,
-        );
-        dev_server_projects::init(client.clone(), cx);
-
-        load_user_themes_in_background(fs.clone(), cx);
-        watch_themes(fs.clone(), cx);
-        watch_languages(fs.clone(), languages.clone(), cx);
-        watch_file_types(fs.clone(), cx);
-
-        languages.set_theme(cx.theme().clone());
-
-        cx.observe_global::<SettingsStore>({
-            let languages = languages.clone();
-            let http = client.http_client();
-            let client = client.clone();
-
-            move |cx| {
-                for &mut window in cx.windows().iter_mut() {
-                    let background_appearance = cx.theme().window_background_appearance();
-                    window
-                        .update(cx, |_, cx| {
-                            cx.set_background_appearance(background_appearance)
-                        })
-                        .ok();
-                }
-                languages.set_theme(cx.theme().clone());
-                let new_host = &client::ClientSettings::get_global(cx).server_url;
-                if &http.base_url() != new_host {
-                    http.set_base_url(new_host);
-                    if client.status().borrow().is_connected() {
-                        client.reconnect(&cx.to_async());
-                    }
-                }
-            }
-        })
-        .detach();
-
         let telemetry = client.telemetry();
         telemetry.start(installation_id.clone(), session_id, cx);
-        telemetry.report_setting_event("theme", cx.theme().name.to_string());
-        telemetry.report_setting_event("keymap", BaseKeymap::get_global(cx).to_string());
         telemetry.report_app_event(
             match existing_installation_id_found {
                 Some(false) => "first open",
@@ -350,7 +365,6 @@ fn init_ui(args: Args) {
             }
             .to_string(),
         );
-        telemetry.flush_events();
         let app_state = Arc::new(AppState {
             languages: languages.clone(),
             client: client.clone(),
@@ -362,44 +376,11 @@ fn init_ui(args: Args) {
         });
         AppState::set_global(Arc::downgrade(&app_state), cx);
 
-        audio::init(Assets, cx);
         auto_update::init(client.http_client(), cx);
 
-        workspace::init(app_state.clone(), cx);
-        recent_projects::init(cx);
-
-        go_to_line::init(cx);
-        file_finder::init(cx);
-        tab_switcher::init(cx);
-        outline::init(cx);
-        project_symbols::init(cx);
-        project_panel::init(Assets, cx);
-        tasks_ui::init(cx);
-        channel::init(&client, user_store.clone(), cx);
-        search::init(cx);
-        vim::init(cx);
-        terminal_view::init(cx);
-
-        journal::init(app_state.clone(), cx);
-        language_selector::init(cx);
-        theme_selector::init(cx);
-        language_tools::init(cx);
-        call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-        notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
-        collab_ui::init(&app_state, cx);
-        feedback::init(cx);
-        markdown_preview::init(cx);
-        welcome::init(cx);
-        extensions_ui::init(cx);
-
-        cx.set_menus(app_menus());
-        initialize_workspace(app_state.clone(), cx);
-
         reliability::init(client.http_client(), installation_id, cx);
 
-        cx.activate(true);
-
-        let mut triggered_authentication = false;
+        let args = Args::parse();
         let urls: Vec<_> = args
             .paths_or_urls
             .iter()
@@ -417,14 +398,30 @@ fn init_ui(args: Args) {
             .and_then(|urls| OpenRequest::parse(urls, cx).log_err())
         {
             Some(request) => {
-                triggered_authentication = handle_open_request(request, app_state.clone(), cx)
+                handle_open_request(request, app_state.clone(), cx);
+            }
+            None => {
+                if let Some(dev_server_token) = args.dev_server_token {
+                    let task =
+                        init_headless(DevServerToken(dev_server_token), app_state.clone(), cx);
+                    cx.spawn(|cx| async move {
+                        if let Err(e) = task.await {
+                            log::error!("{}", e);
+                            cx.update(|cx| cx.quit()).log_err();
+                        } else {
+                            log::info!("connected!");
+                        }
+                    })
+                    .detach();
+                } else {
+                    init_ui(app_state.clone(), cx).unwrap();
+                    cx.spawn({
+                        let app_state = app_state.clone();
+                        |cx| async move { restore_or_create_workspace(app_state, cx).await }
+                    })
+                    .detach();
+                }
             }
-            None => cx
-                .spawn({
-                    let app_state = app_state.clone();
-                    |cx| async move { restore_or_create_workspace(app_state, cx).await }
-                })
-                .detach(),
         }
 
         let app_state = app_state.clone();
@@ -439,34 +436,20 @@ fn init_ui(args: Args) {
             }
         })
         .detach();
-
-        if !triggered_authentication {
-            cx.spawn(|cx| async move { authenticate(client, &cx).await })
-                .detach_and_log_err(cx);
-        }
     });
 }
 
-fn main() {
-    let mut args = Args::parse();
-    if let Some(dev_server_token) = args.dev_server_token.take() {
-        let dev_server_token = DevServerToken(dev_server_token);
-        init_headless(dev_server_token)
-    } else {
-        init_ui(args)
-    }
-}
-
-fn handle_open_request(
-    request: OpenRequest,
-    app_state: Arc<AppState>,
-    cx: &mut AppContext,
-) -> bool {
+fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut AppContext) {
     if let Some(connection) = request.cli_connection {
         let app_state = app_state.clone();
         cx.spawn(move |cx| handle_cli_connection(connection, app_state, cx))
             .detach();
-        return false;
+        return;
+    }
+
+    if let Err(e) = init_ui(app_state.clone(), cx) {
+        log::error!("{}", e);
+        return;
     }
 
     let mut task = None;
@@ -531,12 +514,8 @@ fn handle_open_request(
             anyhow::Ok(())
         })
         .detach_and_log_err(cx);
-        true
-    } else {
-        if let Some(task) = task {
-            task.detach_and_log_err(cx)
-        }
-        false
+    } else if let Some(task) = task {
+        task.detach_and_log_err(cx)
     }
 }
 

crates/zed/src/zed.rs 🔗

@@ -1,6 +1,7 @@
 mod app_menus;
 pub mod inline_completion_registry;
-mod only_instance;
+#[cfg(not(target_os = "linux"))]
+pub(crate) mod only_instance;
 mod open_listener;
 
 pub use app_menus::*;
@@ -12,7 +13,6 @@ use gpui::{
     actions, point, px, AppContext, AsyncAppContext, Context, FocusableView, PromptLevel,
     TitlebarOptions, View, ViewContext, VisualContext, WindowKind, WindowOptions,
 };
-pub use only_instance::*;
 pub use open_listener::*;
 
 use anyhow::Context as _;

crates/zed/src/zed/open_listener.rs 🔗

@@ -13,13 +13,15 @@ use language::{Bias, Point};
 use std::path::Path;
 use std::path::PathBuf;
 use std::sync::Arc;
-use std::thread;
 use std::time::Duration;
+use std::{process, thread};
 use util::paths::PathLikeWithPosition;
 use util::ResultExt;
 use workspace::item::ItemHandle;
 use workspace::{AppState, Workspace};
 
+use crate::{init_headless, init_ui};
+
 #[derive(Default, Debug)]
 pub struct OpenRequest {
     pub cli_connection: Option<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)>,
@@ -116,6 +118,24 @@ impl OpenListener {
     }
 }
 
+#[cfg(target_os = "linux")]
+pub fn listen_for_cli_connections(opener: Arc<OpenListener>) -> Result<()> {
+    use release_channel::RELEASE_CHANNEL_NAME;
+    use std::os::{linux::net::SocketAddrExt, unix::net::SocketAddr, unix::net::UnixDatagram};
+
+    let uid: u32 = unsafe { libc::getuid() };
+    let sock_addr =
+        SocketAddr::from_abstract_name(format!("zed-{}-{}", *RELEASE_CHANNEL_NAME, uid))?;
+    let listener = UnixDatagram::bind_addr(&sock_addr)?;
+    thread::spawn(move || {
+        let mut buf = [0u8; 1024];
+        while let Ok(len) = listener.recv(&mut buf) {
+            opener.open_urls(vec![String::from_utf8_lossy(&buf[..len]).to_string()]);
+        }
+    });
+    Ok(())
+}
+
 fn connect_to_cli(
     server_name: &str,
 ) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
@@ -211,7 +231,50 @@ pub async fn handle_cli_connection(
                 paths,
                 wait,
                 open_new_workspace,
+                dev_server_token,
             } => {
+                if let Some(dev_server_token) = dev_server_token {
+                    match cx
+                        .update(|cx| {
+                            init_headless(client::DevServerToken(dev_server_token), app_state, cx)
+                        })
+                        .unwrap()
+                        .await
+                    {
+                        Ok(_) => {
+                            responses
+                                .send(CliResponse::Stdout {
+                                    message: format!("zed (pid {}) connected!", process::id()),
+                                })
+                                .log_err();
+                            responses.send(CliResponse::Exit { status: 0 }).log_err();
+                        }
+                        Err(error) => {
+                            responses
+                                .send(CliResponse::Stderr {
+                                    message: format!("{}", error),
+                                })
+                                .log_err();
+                            responses.send(CliResponse::Exit { status: 1 }).log_err();
+                            cx.update(|cx| cx.quit()).log_err();
+                        }
+                    }
+                    return;
+                }
+
+                if let Err(e) = cx
+                    .update(|cx| init_ui(app_state.clone(), cx))
+                    .and_then(|r| r)
+                {
+                    responses
+                        .send(CliResponse::Stderr {
+                            message: format!("{}", e),
+                        })
+                        .log_err();
+                    responses.send(CliResponse::Exit { status: 1 }).log_err();
+                    return;
+                }
+
                 let paths = if paths.is_empty() {
                     if open_new_workspace == Some(true) {
                         vec![]

script/bundle-linux 🔗

@@ -38,11 +38,12 @@ host_line=$(echo "$version_info" | grep host)
 target_triple=${host_line#*: }
 
 # Build binary in release mode
-cargo build --release --target "${target_triple}" --package zed
+cargo build --release --target "${target_triple}" --package zed --package cli
 
 # Strip the binary of all debug symbols
 # Later, we probably want to do something like this: https://github.com/GabrielMajeri/separate-symbols
 strip "target/${target_triple}/release/Zed"
+strip "target/${target_triple}/release/cli"
 
 suffix=""
 if [ "$channel" != "stable" ]; then
@@ -57,6 +58,7 @@ zed_dir="${temp_dir}/zed$suffix.app"
 # Binary
 mkdir -p "${zed_dir}/bin"
 cp "target/${target_triple}/release/Zed" "${zed_dir}/bin/zed"
+cp "target/${target_triple}/release/cli" "${zed_dir}/bin/cli"
 
 # Icons
 mkdir -p "${zed_dir}/share/icons/hicolor/512x512/apps"

script/install.sh 🔗

@@ -80,7 +80,7 @@ linux() {
     mkdir -p "$HOME/.local/bin" "$HOME/.local/share/applications"
 
     # Link the binary
-    ln -sf ~/.local/zed$suffix.app/bin/zed "$HOME/.local/bin/zed"
+    ln -sf ~/.local/zed$suffix.app/bin/cli "$HOME/.local/bin/zed"
 
     # Copy .desktop file
     desktop_file_path="$HOME/.local/share/applications/${appid}.desktop"