WIP: ssh remoting: Add upload_binary field to SshConnections (#19748)
Some checks are pending
CI / Check Postgres and Protobuf migrations, mergability (push) Waiting to run
CI / Check formatting and spelling (push) Waiting to run
CI / (macOS) Run Clippy and tests (push) Waiting to run
CI / (Linux) Run Clippy and tests (push) Waiting to run
CI / (Linux) Build Remote Server (push) Waiting to run
CI / (Windows) Run Clippy and tests (push) Waiting to run
CI / Create a macOS bundle (push) Blocked by required conditions
CI / Create a Linux bundle (push) Blocked by required conditions
CI / Create arm64 Linux bundle (push) Blocked by required conditions
Deploy Docs / Deploy Docs (push) Waiting to run
Docs / Check formatting (push) Waiting to run

This removes the old `remote_server { "download_binary_on_host": bool }`
field and replaces it with a `upload_binary: bool` on every
`ssh_connection`.


@ConradIrwin it compiles, it connects, but I haven't tested it really
yet

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Thorsten Ball 2024-10-26 01:32:54 +02:00 committed by GitHub
parent 1acebb3c47
commit fc8a72cdd8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 204 additions and 235 deletions

View file

@ -11,7 +11,7 @@ use ui::{
};
use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
use crate::{open_ssh_project, SshSettings};
use crate::open_ssh_project;
enum Host {
RemoteProject,
@ -102,16 +102,6 @@ impl DisconnectedOverlay {
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
cx.spawn(move |_, mut cx| async move {
let nickname = cx
.update(|cx| {
SshSettings::get_global(cx).nickname_for(
&connection_options.host,
connection_options.port,
&connection_options.username,
)
})
.ok()
.flatten();
open_ssh_project(
connection_options,
paths,
@ -120,7 +110,6 @@ impl DisconnectedOverlay {
replace_window: Some(window),
..Default::default()
},
nickname,
&mut cx,
)
.await?;

View file

@ -1,7 +1,6 @@
pub mod disconnected_overlay;
mod remote_servers;
mod ssh_connections;
use remote::SshConnectionOptions;
pub use ssh_connections::open_ssh_project;
use disconnected_overlay::DisconnectedOverlay;
@ -331,23 +330,12 @@ impl PickerDelegate for RecentProjectsDelegate {
..Default::default()
};
let args = SshSettings::get_global(cx).args_for(
&ssh_project.host,
ssh_project.port,
&ssh_project.user,
);
let nickname = SshSettings::get_global(cx).nickname_for(
&ssh_project.host,
ssh_project.port,
&ssh_project.user,
);
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
port: ssh_project.port,
password: None,
args,
};
let connection_options = SshSettings::get_global(cx)
.connection_options_for(
ssh_project.host.clone(),
ssh_project.port,
ssh_project.user.clone(),
);
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
@ -357,7 +345,6 @@ impl PickerDelegate for RecentProjectsDelegate {
paths,
app_state,
open_options,
nickname,
&mut cx,
)
.await

View file

@ -197,11 +197,7 @@ impl ProjectPicker {
picker
});
let connection_string = connection.connection_string().into();
let nickname = SshSettings::get_global(cx).nickname_for(
&connection.host,
connection.port,
&connection.username,
);
let nickname = connection.nickname.clone().map(|nick| nick.into());
let _path_task = cx
.spawn({
let workspace = workspace.clone();
@ -414,7 +410,7 @@ impl RemoteServerProjects {
return;
}
};
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, None, cx));
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
let connection = connect_over_ssh(
connection_options.remote_server_identifier(),
@ -491,12 +487,11 @@ impl RemoteServerProjects {
return;
};
let nickname = ssh_connection.nickname.clone();
let connection_options = ssh_connection.into();
workspace.update(cx, |_, cx| {
cx.defer(move |workspace, cx| {
workspace.toggle_modal(cx, |cx| {
SshConnectionModal::new(&connection_options, Vec::new(), nickname, cx)
SshConnectionModal::new(&connection_options, Vec::new(), cx)
});
let prompt = workspace
.active_modal::<SshConnectionModal>(cx)
@ -584,9 +579,7 @@ impl RemoteServerProjects {
self.create_ssh_server(state.address_editor.clone(), cx);
}
Mode::EditNickname(state) => {
let text = Some(state.editor.read(cx).text(cx))
.filter(|text| !text.is_empty())
.map(SharedString::from);
let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty());
let index = state.index;
self.update_settings_file(cx, move |setting, _| {
if let Some(connections) = setting.ssh_connections.as_mut() {
@ -633,7 +626,7 @@ impl RemoteServerProjects {
) -> impl IntoElement {
let (main_label, aux_label) = if let Some(nickname) = ssh_connection.nickname.clone() {
let aux_label = SharedString::from(format!("({})", ssh_connection.host));
(nickname, Some(aux_label))
(nickname.into(), Some(aux_label))
} else {
(ssh_connection.host.clone(), None)
};
@ -746,13 +739,11 @@ impl RemoteServerProjects {
let project = project.clone();
let server = server.clone();
cx.spawn(|remote_server_projects, mut cx| async move {
let nickname = server.nickname.clone();
let result = open_ssh_project(
server.into(),
project.paths.into_iter().map(PathBuf::from).collect(),
app_state,
OpenOptions::default(),
nickname,
&mut cx,
)
.await;
@ -861,6 +852,7 @@ impl RemoteServerProjects {
projects: vec![],
nickname: None,
args: connection_options.args.unwrap_or_default(),
upload_binary_over_ssh: None,
})
});
}
@ -953,7 +945,7 @@ impl RemoteServerProjects {
SshConnectionHeader {
connection_string: connection_string.clone(),
paths: Default::default(),
nickname: connection.nickname.clone(),
nickname: connection.nickname.clone().map(|s| s.into()),
}
.render(cx),
)
@ -1135,13 +1127,14 @@ impl RemoteServerProjects {
};
let connection_string = connection.host.clone();
let nickname = connection.nickname.clone().map(|s| s.into());
v_flex()
.child(
SshConnectionHeader {
connection_string,
paths: Default::default(),
nickname: connection.nickname.clone(),
nickname,
}
.render(cx),
)

View file

@ -26,15 +26,9 @@ use ui::{
};
use workspace::{AppState, ModalView, Workspace};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct RemoteServerSettings {
pub download_on_host: Option<bool>,
}
#[derive(Deserialize)]
pub struct SshSettings {
pub ssh_connections: Option<Vec<SshConnection>>,
pub remote_server: Option<RemoteServerSettings>,
}
impl SshSettings {
@ -42,39 +36,31 @@ impl SshSettings {
self.ssh_connections.clone().into_iter().flatten()
}
pub fn args_for(
pub fn connection_options_for(
&self,
host: &str,
host: String,
port: Option<u16>,
user: &Option<String>,
) -> Option<Vec<String>> {
self.ssh_connections()
.filter_map(|conn| {
if conn.host == host && &conn.username == user && conn.port == port {
Some(conn.args)
} else {
None
}
})
.next()
}
pub fn nickname_for(
&self,
host: &str,
port: Option<u16>,
user: &Option<String>,
) -> Option<SharedString> {
self.ssh_connections()
.filter_map(|conn| {
if conn.host == host && &conn.username == user && conn.port == port {
Some(conn.nickname)
} else {
None
}
})
.next()
.flatten()
username: Option<String>,
) -> SshConnectionOptions {
for conn in self.ssh_connections() {
if conn.host == host && conn.username == username && conn.port == port {
return SshConnectionOptions {
nickname: conn.nickname,
upload_binary_over_ssh: conn.upload_binary_over_ssh.unwrap_or_default(),
args: Some(conn.args),
host,
port,
username,
password: None,
};
}
}
SshConnectionOptions {
host,
port,
username,
..Default::default()
}
}
}
@ -85,13 +71,20 @@ pub struct SshConnection {
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub port: Option<u16>,
pub projects: Vec<SshProject>,
/// Name to use for this server in UI.
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<SharedString>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub projects: Vec<SshProject>,
/// Name to use for this server in UI.
#[serde(skip_serializing_if = "Option::is_none")]
pub nickname: Option<String>,
// By default Zed will download the binary to the host directly.
// If this is set to true, Zed will download the binary to your local machine,
// and then upload it over the SSH connection. Useful if your SSH server has
// limited outbound internet access.
#[serde(skip_serializing_if = "Option::is_none")]
pub upload_binary_over_ssh: Option<bool>,
}
impl From<SshConnection> for SshConnectionOptions {
@ -102,6 +95,8 @@ impl From<SshConnection> for SshConnectionOptions {
port: val.port,
password: None,
args: Some(val.args),
nickname: val.nickname,
upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(),
}
}
}
@ -114,7 +109,6 @@ pub struct SshProject {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct RemoteSettingsContent {
pub ssh_connections: Option<Vec<SshConnection>>,
pub remote_server: Option<RemoteServerSettings>,
}
impl Settings for SshSettings {
@ -153,10 +147,10 @@ pub struct SshConnectionModal {
impl SshPrompt {
pub(crate) fn new(
connection_options: &SshConnectionOptions,
nickname: Option<SharedString>,
cx: &mut ViewContext<Self>,
) -> Self {
let connection_string = connection_options.connection_string().into();
let nickname = connection_options.nickname.clone().map(|s| s.into());
Self {
connection_string,
@ -276,11 +270,10 @@ impl SshConnectionModal {
pub(crate) fn new(
connection_options: &SshConnectionOptions,
paths: Vec<PathBuf>,
nickname: Option<SharedString>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
prompt: cx.new_view(|cx| SshPrompt::new(connection_options, nickname, cx)),
prompt: cx.new_view(|cx| SshPrompt::new(connection_options, cx)),
finished: false,
paths,
}
@ -451,13 +444,17 @@ impl remote::SshClientDelegate for SshClientDelegate {
fn get_server_binary(
&self,
platform: SshPlatform,
upload_binary_over_ssh: bool,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, SemanticVersion)>> {
let (tx, rx) = oneshot::channel();
let this = self.clone();
cx.spawn(|mut cx| async move {
tx.send(this.get_server_binary_impl(platform, &mut cx).await)
.ok();
tx.send(
this.get_server_binary_impl(platform, upload_binary_over_ssh, &mut cx)
.await,
)
.ok();
})
.detach();
rx
@ -492,19 +489,14 @@ impl SshClientDelegate {
async fn get_server_binary_impl(
&self,
platform: SshPlatform,
upload_binary_via_ssh: bool,
cx: &mut AsyncAppContext,
) -> Result<(ServerBinary, SemanticVersion)> {
let (version, release_channel, download_binary_on_host) = cx.update(|cx| {
let (version, release_channel) = cx.update(|cx| {
let version = AppVersion::global(cx);
let channel = ReleaseChannel::global(cx);
let ssh_settings = SshSettings::get_global(cx);
let download_binary_on_host = ssh_settings
.remote_server
.as_ref()
.and_then(|server| server.download_on_host)
.unwrap_or(false);
(version, channel, download_binary_on_host)
(version, channel)
})?;
// In dev mode, build the remote server binary from source
@ -529,33 +521,7 @@ impl SshClientDelegate {
cx,
);
if download_binary_on_host {
let (request_url, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
current_version,
cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
version,
platform.os,
platform.arch,
e
)
})?;
Ok((
ServerBinary::ReleaseUrl {
url: request_url,
body: request_body,
},
version,
))
} else {
if upload_binary_via_ssh {
let binary_path = AutoUpdater::download_remote_server_release(
platform.os,
platform.arch,
@ -575,6 +541,32 @@ impl SshClientDelegate {
})?;
Ok((ServerBinary::LocalBinary(binary_path), version))
} else {
let (request_url, request_body) = AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
current_version,
cx,
)
.await
.map_err(|e| {
anyhow!(
"Failed to get remote server binary download url (version: {}, os: {}, arch: {}): {}",
version,
platform.os,
platform.arch,
e
)
})?;
Ok((
ServerBinary::ReleaseUrl {
url: request_url,
body: request_body,
},
version,
))
}
}
@ -715,7 +707,6 @@ pub async fn open_ssh_project(
paths: Vec<PathBuf>,
app_state: Arc<AppState>,
open_options: workspace::OpenOptions,
nickname: Option<SharedString>,
cx: &mut AsyncAppContext,
) -> Result<()> {
let window = if let Some(window) = open_options.replace_window {
@ -740,12 +731,11 @@ pub async fn open_ssh_project(
let (cancel_tx, cancel_rx) = oneshot::channel();
let delegate = window.update(cx, {
let connection_options = connection_options.clone();
let nickname = nickname.clone();
let paths = paths.clone();
move |workspace, cx| {
cx.activate_window();
workspace.toggle_modal(cx, |cx| {
SshConnectionModal::new(&connection_options, paths, nickname.clone(), cx)
SshConnectionModal::new(&connection_options, paths, cx)
});
let ui = workspace

View file

@ -64,6 +64,9 @@ pub struct SshConnectionOptions {
pub port: Option<u16>,
pub password: Option<String>,
pub args: Option<Vec<String>>,
pub nickname: Option<String>,
pub upload_binary_over_ssh: bool,
}
impl SshConnectionOptions {
@ -141,8 +144,10 @@ impl SshConnectionOptions {
host: hostname.to_string(),
username: username.clone(),
port,
password: None,
args: Some(args),
password: None,
nickname: None,
upload_binary_over_ssh: false,
})
}
@ -236,6 +241,7 @@ pub trait SshClientDelegate: Send + Sync {
fn get_server_binary(
&self,
platform: SshPlatform,
upload_binary_over_ssh: bool,
cx: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, SemanticVersion)>>;
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext);
@ -1705,7 +1711,10 @@ impl SshRemoteConnection {
return Ok(());
}
let (binary, version) = delegate.get_server_binary(platform, cx).await??;
let upload_binary_over_ssh = self.socket.connection_options.upload_binary_over_ssh;
let (binary, version) = delegate
.get_server_binary(platform, upload_binary_over_ssh, cx)
.await??;
let mut remote_version = None;
if cfg!(not(debug_assertions)) {
@ -2336,6 +2345,7 @@ mod fake {
fn get_server_binary(
&self,
_: SshPlatform,
_: bool,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(ServerBinary, SemanticVersion)>> {
unreachable!()

View file

@ -23,7 +23,6 @@ test-support = [
"gpui/test-support",
"http_client/test-support",
"project/test-support",
"settings/test-support",
"util/test-support",
"workspace/test-support",
]
@ -43,7 +42,6 @@ recent_projects.workspace = true
remote.workspace = true
rpc.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true

View file

@ -18,10 +18,8 @@ use gpui::{
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
use recent_projects::{OpenRemote, RecentProjects, SshSettings};
use remote::SshConnectionOptions;
use recent_projects::{OpenRemote, RecentProjects};
use rpc::proto;
use settings::Settings;
use smallvec::SmallVec;
use std::sync::Arc;
use theme::ActiveTheme;
@ -29,7 +27,7 @@ use ui::{
h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
};
use util::{maybe, ResultExt};
use util::ResultExt;
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{notifications::NotifyResultExt, Workspace};
@ -268,15 +266,11 @@ impl TitleBar {
let options = self.project.read(cx).ssh_connection_options(cx)?;
let host: SharedString = options.connection_string().into();
let nickname = maybe!({
SshSettings::get_global(cx)
.ssh_connections
.as_ref()?
.into_iter()
.find(|connection| SshConnectionOptions::from((*connection).clone()) == options)
.and_then(|connection| connection.nickname.clone())
})
.unwrap_or_else(|| host.clone());
let nickname = options
.nickname
.clone()
.map(|nick| nick.into())
.unwrap_or_else(|| host.clone());
let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),

View file

@ -25,7 +25,6 @@ use gpui::{
use http_client::{read_proxy_from_env, Uri};
use language::LanguageRegistry;
use log::LevelFilter;
use remote::SshConnectionOptions;
use reqwest_client::ReqwestClient;
use assets::Assets;
@ -616,26 +615,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
return;
}
if let Some(connection_info) = request.ssh_connection {
if let Some(connection_options) = request.ssh_connection {
cx.spawn(|mut cx| async move {
let nickname = cx
.update(|cx| {
SshSettings::get_global(cx).nickname_for(
&connection_info.host,
connection_info.port,
&connection_info.username,
)
})
.ok()
.flatten();
let paths_with_position =
derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
open_ssh_project(
connection_info,
connection_options,
paths_with_position.into_iter().map(|p| p.path).collect(),
app_state,
workspace::OpenOptions::default(),
nickname,
&mut cx,
)
.await
@ -798,25 +786,10 @@ async fn restore_or_create_workspace(
task.await?;
}
SerializedWorkspaceLocation::Ssh(ssh) => {
let args = cx
.update(|cx| {
SshSettings::get_global(cx).args_for(&ssh.host, ssh.port, &ssh.user)
})
.ok()
.flatten();
let nickname = cx
.update(|cx| {
SshSettings::get_global(cx).nickname_for(&ssh.host, ssh.port, &ssh.user)
})
.ok()
.flatten();
let connection_options = SshConnectionOptions {
args,
host: ssh.host.clone(),
username: ssh.user.clone(),
port: ssh.port,
password: None,
};
let connection_options = cx.update(|cx| {
SshSettings::get_global(cx)
.connection_options_for(ssh.host, ssh.port, ssh.user)
})?;
let app_state = app_state.clone();
cx.spawn(move |mut cx| async move {
recent_projects::open_ssh_project(
@ -824,7 +797,6 @@ async fn restore_or_create_workspace(
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
workspace::OpenOptions::default(),
nickname,
&mut cx,
)
.await

View file

@ -881,12 +881,6 @@ pub fn open_new_ssh_project_from_project(
return Task::ready(Err(anyhow::anyhow!("Not an ssh project")));
};
let connection_options = ssh_client.read(cx).connection_options();
let nickname = recent_projects::SshSettings::get_global(cx).nickname_for(
&connection_options.host,
connection_options.port,
&connection_options.username,
);
cx.spawn(|_, mut cx| async move {
open_ssh_project(
connection_options,
@ -897,7 +891,6 @@ pub fn open_new_ssh_project_from_project(
replace_window: None,
env: None,
},
nickname,
&mut cx,
)
.await

View file

@ -72,25 +72,24 @@ impl OpenRequest {
.ok_or_else(|| anyhow!("missing host in ssh url: {}", file))?
.to_string();
let username = Some(url.username().to_string()).filter(|s| !s.is_empty());
let password = url.password().map(|s| s.to_string());
let port = url.port();
if !self.open_paths.is_empty() {
return Err(anyhow!("cannot open both local and ssh paths"));
}
let args = SshSettings::get_global(cx).args_for(&host, port, &username);
let connection = SshConnectionOptions {
username,
password,
host,
let mut connection_options = SshSettings::get_global(cx).connection_options_for(
host.clone(),
port,
args,
};
username.clone(),
);
if let Some(password) = url.password() {
connection_options.password = Some(password.to_string());
}
if let Some(ssh_connection) = &self.ssh_connection {
if *ssh_connection != connection {
if *ssh_connection != connection_options {
return Err(anyhow!("cannot open multiple ssh connections"));
}
}
self.ssh_connection = Some(connection);
self.ssh_connection = Some(connection_options);
self.parse_file_path(url.path());
Ok(())
}
@ -374,40 +373,28 @@ async fn open_workspaces(
}
SerializedWorkspaceLocation::Ssh(ssh) => {
let app_state = app_state.clone();
let args = cx
.update(|cx| {
SshSettings::get_global(cx).args_for(&ssh.host, ssh.port, &ssh.user)
let connection_options = cx.update(|cx| {
SshSettings::get_global(cx)
.connection_options_for(ssh.host, ssh.port, ssh.user)
});
if let Ok(connection_options) = connection_options {
cx.spawn(|mut cx| async move {
open_ssh_project(
connection_options,
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
OpenOptions::default(),
&mut cx,
)
.await
.log_err();
})
.ok()
.flatten();
let connection_options = SshConnectionOptions {
args,
host: ssh.host.clone(),
username: ssh.user.clone(),
port: ssh.port,
password: None,
};
let nickname = cx
.update(|cx| {
SshSettings::get_global(cx).nickname_for(&ssh.host, ssh.port, &ssh.user)
})
.ok()
.flatten();
cx.spawn(|mut cx| async move {
open_ssh_project(
connection_options,
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
OpenOptions::default(),
nickname,
&mut cx,
)
.await
.log_err();
})
.detach();
// We don't set `errored` here, because for ssh projects, the
// error is displayed in the window.
.detach();
// We don't set `errored` here if `open_ssh_project` fails, because for ssh projects, the
// error is displayed in the window.
} else {
errored = false;
}
}
}
}

View file

@ -8,7 +8,7 @@ Remote Development allows you to code at the speed of thought, even when your co
Remote development requires two computers, your local machine that runs the Zed UI and the remote server which runs a Zed headless server. The two communicate over SSH, so you will need to be able to SSH from your local machine into the remote server to use this feature.
> **Note:** The original version of remote development sent traffic via Zed's servers. As of Zed v0.157 you can no-longer use this mode.
> **Note:** The original version of remote development sent traffic via Zed's servers. As of Zed v0.157 you can no-longer use that mode.
## Setup
@ -29,7 +29,63 @@ The remote machine must be able to run Zed's server. The following platforms sho
- Linux (x86_64 or arm64, we do not yet support 32-bit platforms)
- Windows is not yet supported.
## Settings
## Configuration
The list of remote servers is stored in your settings file {#kb zed::OpenSettings}. You can edit this list using the Remote Projects dialogue {#kb projects::OpenRemote}, which provides some robustness - for example it checks that the connection can be established before writing it to the settings file.
```json
{
"ssh_connections": [
{
"host": "192.168.1.10",
"projects": ["~/code/zed/zed"]
}
]
}
```
Zed shells out to the `ssh` on your path, and so it will inherit any configuration you have in `~/.ssh/config` for the given host. That said, if you need to override anything you can configure the following additional options on each connection:
```json
{
"ssh_connections": [
{
"host": "192.168.1.10",
"projects": ["~/code/zed/zed"],
// any argument to pass to the ssh master process
"args": ["-i", "~/.ssh/work_id_file"],
"port": 22, // defaults to 22
// defaults to your username on your local machine
"username": "me"
}
]
}
```
There are two additional Zed-specific options per connection, `upload_binary_over_ssh` and `nickname`:
```json
{
"ssh_connections": [
{
"host": "192.168.1.10",
"projects": ["~/code/zed/zed"],
// by default Zed will download the server binary from the internet on the remote.
// When this is true, it'll be downloaded to your laptop and uploaded over SSH.
// This is useful when your remote server has restricted internet access.
"upload_binary_over_ssh": true,
// Shown in the Zed UI to help distinguish multiple hosts.
"nickname": "lil-linux"
}
]
}
```
If you use the command line to open a connection to a host by doing `zed ssh://192.168.1.10/~/.vimrc`, then extra options are read from your settings file by finding the first connection that matches the host/username/port of the URL on the command line.
Additionally it's worth noting that while you can pass a password on the command line `zed ssh://user:password@host/~`, we do not support writing a password to your settings file. If you're connecting repeatedly to the same host, you should configure key-based authentication.
## Zed settings
When opening a remote project there are three relevant settings locations: