//! The Connections page, in T3 Code's layout: "This machine" (letting other
//! devices connect to this one) and "Environments" (the devices this one
//! connects to), with "Add environment" for pairing a new one.
use gpui_kit::component::{
Icon, IconName, WindowExt as _, h_flex,
input::{InputEvent, InputState},
notification::Notification,
v_flex,
};
use gpui_kit::prelude::FluentBuilder as _;
use gpui_kit::*;
use super::SettingsView;
use super::widgets::{Section, SettingRow, WIDE_WIDTH, empty_row, focus_later, switch, text_field};
use crate::remote::{self, DEFAULT_PORT, client::ConnectionStatus};
use crate::settings::{self, RemoteHost, Settings};
use crate::ui::icons::Lucide;
use crate::ui::metal::{self, Led, MetalButton, MetalStyled as _};
/// The page's fields.
pub(super) struct ConnectionsState {
remote_port: Entity<InputState>,
/// The "Add environment" form: name, address or pairing code, key.
connect_name: Entity<InputState>,
connect_address: Entity<InputState>,
connect_key: Entity<InputState>,
adding: bool,
show_key: bool,
}
impl ConnectionsState {
pub fn new(window: &mut Window, cx: &mut Context<SettingsView>, subscriptions: &mut Vec<Subscription>) -> Self {
let remote_port = cx.new(|cx| InputState::new(window, cx).placeholder(DEFAULT_PORT.to_string()));
subscriptions.push(cx.subscribe_in(&remote_port, window, |this, input, event: &InputEvent, window, cx| {
if matches!(event, InputEvent::Blur | InputEvent::PressEnter { .. }) {
let port = input.read(cx).value().trim().parse::<u16>().ok().filter(|port| *port > 0).unwrap_or(DEFAULT_PORT);
if settings::get(|settings| settings.remote_port != port) {
settings::update(|settings| settings.remote_port = port);
this.changed(window, cx);
}
}
}));
Self {
remote_port,
connect_name: cx.new(|cx| InputState::new(window, cx).placeholder("Name (optional), such as Studio desktop")),
connect_address: cx.new(|cx| InputState::new(window, cx).placeholder("Pairing code, or 192.168.2.10")),
connect_key: cx.new(|cx| InputState::new(window, cx).placeholder("Key (not needed with a pairing code)").masked(true)),
adding: false,
show_key: false,
}
}
pub fn reload(&mut self, saved: &Settings, window: &mut Window, cx: &mut Context<SettingsView>) {
let port = saved.remote_port.to_string();
self.remote_port.update(cx, |input, cx| input.set_value(port, window, cx));
}
}
impl SettingsView {
/// Saves the "Add environment" form as a new connection.
fn add_connection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let state = &self.connections;
let value = |input: &Entity<InputState>| input.read(cx).value().trim().to_string();
let (address, code_key) = remote::parse_pairing(&value(&state.connect_address));
let key = Some(value(&state.connect_key)).filter(|key| !key.is_empty()).or(code_key);
let (Some(key), false) = (key, address.is_empty()) else {
window.push_notification(Notification::error("Enter the other device's pairing code, or its address and key."), cx);
return;
};
let name = value(&state.connect_name);
let ids: Vec<String> = settings::get(|settings| settings.remotes.iter().map(|host| host.id.clone()).collect());
let id = (1..).map(|n| format!("remote-{n}")).find(|id| !ids.contains(id)).unwrap_or_default();
settings::update(|settings| settings.remotes.push(RemoteHost { id, name, address, key, enabled: true, settings_offered: false }));
for input in [&state.connect_name, &state.connect_address, &state.connect_key] {
input.update(cx, |input, cx| input.set_value("", window, cx));
}
self.connections.adding = false;
self.changed(window, cx);
}
fn edit_remote(&mut self, window: &mut Window, cx: &mut Context<Self>, change: impl FnOnce(&mut Vec<RemoteHost>)) {
settings::update(|settings| change(&mut settings.remotes));
self.changed(window, cx);
}
pub(super) fn render_connections(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
let p = metal::palette(cx);
let saved = settings::load();
let view = cx.weak_entity();
let overview = self.workspace.upgrade().map(|workspace| workspace.read(cx).remote_overview());
let (serving, error, peers, statuses) = match overview {
Some(overview) => (overview.serving, overview.error, overview.peers, overview.environments),
None => (None, None, Vec::new(), Vec::new()),
};
let addresses: Vec<String> =
remote::local_addresses().into_iter().map(|address| format!("{address}:{}", saved.remote_port)).collect();
let row = |title: &'static str| SettingRow::new(title).page_width(WIDE_WIDTH);
let mut machine = Section::new("This machine")
.id("connections-environment")
.icon(Icon::new(Lucide::Laptop).size(px(14.)).text_color(p.ink2))
.description("Connections aren't encrypted. Allow them on networks you trust, or over a VPN such as Tailscale or WireGuard.")
.row(
row("Allow remote connections")
.description(
"Other devices running Vectrum Code can open this device's projects and threads and work in them, with this device's agents and files.",
)
.control(switch("remote-server", saved.remote_server, &view, p, |settings, on| settings.remote_server = on)),
);
if saved.remote_server {
let (led, status) = match (&error, serving) {
(Some(error), _) => (Led::Failed, error.clone()),
(None, Some(port)) => (
Led::Live,
match addresses.as_slice() {
[] => format!("Listening on port {port}."),
addresses => format!("Listening at {}.", addresses.join(", ")),
},
),
(None, None) => (Led::Waiting, "Starting…".to_string()),
};
machine = machine
.row(row("Status").description(status).control(metal::led(
"remote-server-led",
led,
serving.is_none() && error.is_none(),
p,
)))
.row(
row("Port")
.description("The port other devices connect to. Your firewall must let it in.")
.control(text_field(&self.connections.remote_port, 110., window, cx)),
);
let key = saved.remote_key.clone();
let shown = if self.connections.show_key {
key.clone()
} else {
format!("{}{}", "•".repeat(12), key.chars().rev().take(4).collect::<Vec<_>>().into_iter().rev().collect::<String>())
};
let copy_key = key.clone();
let show_key = self.connections.show_key;
machine = machine.row(
row("Key")
.description("Other devices need it to connect. A new key disconnects everyone using the old one.")
.control(
h_flex()
.gap(px(6.))
.child(
div()
.px(px(10.))
.py(px(6.))
.rounded(px(10.))
.well(p)
.font_family(metal::MONO)
.text_size(px(11.5))
.text_color(p.ink2)
.max_w(px(230.))
.truncate()
.child(shown),
)
.child(
MetalButton::new("show-key")
.ghost()
.mini()
.icon(if show_key { IconName::EyeOff } else { IconName::Eye })
.tooltip(if show_key { "Hide" } else { "Show" })
.on_click(cx.listener(|this, _, _, cx| {
this.connections.show_key = !this.connections.show_key;
cx.notify();
})),
)
.child(
MetalButton::new("copy-key")
.ghost()
.mini()
.icon(IconName::Copy)
.tooltip("Copy key")
.on_click(move |_, _, cx| cx.write_to_clipboard(ClipboardItem::new_string(copy_key.clone()))),
)
.child(
MetalButton::new("new-key")
.ghost()
.mini()
.icon(Lucide::RotateCcw)
.tooltip("Make a new key")
.on_click(cx.listener(|this, _, window, cx| {
settings::update(|settings| settings.remote_key = remote::new_key());
this.changed(window, cx);
})),
),
),
);
if let Some(address) = addresses.first() {
let code = remote::pairing_code(address, &key);
let mut pairing = row("Pairing code")
.description("Paste it into Settings → Connections on the other device: the address and key in one.");
if let Some(qr) = pairing_qr(&format!("vectrum://{code}"), p) {
pairing = pairing.below(
h_flex().gap(px(14.)).child(qr).child(
div()
.max_w(px(200.))
.text_size(px(12.))
.line_height(px(18.))
.text_color(p.ink3)
.child("Scan with Vectrum Code for Android to connect your phone."),
),
);
}
machine = machine.row(
pairing
.control(
MetalButton::new("copy-pairing").compact().icon(IconName::Copy).label("Copy pairing code").on_click(
move |_, window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));
window.push_notification(Notification::success("Pairing code copied."), cx);
},
),
),
);
}
if peers.is_empty() {
machine = machine.row(row("Connected devices").description("None right now."));
}
for peer in peers {
let workspace = self.workspace.clone();
let conn = peer.conn;
machine = machine.row(
row("Connected device").description(format!("{} · {}", peer.name, peer.address)).control(
MetalButton::new(("disconnect-peer", conn as usize)).ghost().compact().label("Disconnect").on_click(
move |_, _, cx| {
let _ = workspace.update(cx, |workspace, cx| workspace.disconnect_peer(conn, cx));
},
),
),
);
}
}
let mut environments = Section::new("Environments").id("remote-environments").header_action(
MetalButton::new("add-environment")
.compact()
.icon(Lucide::Plus)
.label("Add environment")
.disabled(self.connections.adding)
.on_click(cx.listener(|this, _, window, cx| {
this.connections.adding = true;
focus_later(&this.connections.connect_address, window);
cx.notify();
})),
);
if saved.remotes.is_empty() && !self.connections.adding {
environments = environments.row(empty_row("No saved remote environments", p));
}
for host in &saved.remotes {
let status = statuses.iter().find(|(id, _)| *id == host.id).map(|(_, status)| status.clone());
let (led, text) = match (&status, host.enabled) {
(_, false) => (Led::Off, "Off".to_string()),
(Some(ConnectionStatus::Connected(name)), _) => (Led::Live, format!("Connected to {name}")),
(Some(ConnectionStatus::Connecting) | None, _) => (Led::Waiting, "Connecting…".to_string()),
(Some(ConnectionStatus::Failed(error)), _) => (Led::Failed, format!("Retrying · {error}")),
(Some(ConnectionStatus::Denied(reason)), _) => (Led::Failed, reason.clone()),
};
let (toggle_id, remove_id) = (host.id.clone(), host.id.clone());
let (toggle_view, remove_view) = (view.clone(), view.clone());
let connecting = status == Some(ConnectionStatus::Connecting);
environments = environments.row(
SettingRow::new(host.label())
.page_width(WIDE_WIDTH)
.title_suffix(metal::led(SharedString::from(format!("remote-led-{}", host.id)), led, connecting, p))
.description(format!("{} · {text}", remote::with_port(&host.address)))
.control(
h_flex()
.gap(px(8.))
.child(metal::toggle(
SharedString::from(format!("remote-enabled-{}", host.id)),
host.enabled,
p,
move |on, window, cx| {
let id = toggle_id.clone();
let _ = toggle_view.update(cx, |this, cx| {
this.edit_remote(window, cx, move |hosts| {
if let Some(host) = hosts.iter_mut().find(|host| host.id == id) {
host.enabled = on;
}
})
});
},
))
.child(
MetalButton::new(SharedString::from(format!("remove-remote-{}", host.id)))
.ghost()
.mini()
.icon(Lucide::Trash)
.tooltip("Remove from this device")
.on_click(move |_, window, cx| {
let id = remove_id.clone();
let _ = remove_view.update(cx, |this, cx| {
this.edit_remote(window, cx, move |hosts| hosts.retain(|host| host.id != id))
});
}),
),
),
);
}
if self.connections.adding {
let state = &self.connections;
environments = environments.row(
v_flex()
.px(px(16.))
.py(px(14.))
.gap(px(10.))
.child(div().text_size(px(14.)).font_weight(FontWeight::MEDIUM).text_color(p.ink).child("Remote link"))
.child(
div()
.text_size(px(12.))
.text_color(p.ink3)
.child("On the other device, turn on Allow remote connections and copy its pairing code, or its address and key."),
)
.child(text_field(&state.connect_address, 0., window, cx).w_full())
.child(
h_flex()
.gap(px(8.))
.child(text_field(&state.connect_name, 0., window, cx).flex_1())
.child(text_field(&state.connect_key, 0., window, cx).flex_1()),
)
.child(
h_flex()
.gap(px(8.))
.child(div().flex_1())
.child(MetalButton::new("cancel-environment").ghost().compact().label("Cancel").on_click(cx.listener(
|this, _, _, cx| {
this.connections.adding = false;
cx.notify();
},
)))
.child(
MetalButton::new("connect-remote")
.primary()
.compact()
.label("Connect")
.on_click(cx.listener(|this, _, window, cx| this.add_connection(window, cx))),
),
),
);
}
vec![machine.render(cx), environments.render(cx)]
}
}
/// A pairing code as a QR code for a phone to scan, on a small raised card.
/// The modules are black on white whatever the theme, since that's what
/// scanners read best, with the four-module quiet zone the standard asks for.
fn pairing_qr(code: &str, p: &metal::Palette) -> Option<Div> {
const QUIET: usize = 4;
let qr = qrcode::QrCode::with_error_correction_level(code, qrcode::EcLevel::L).ok()?;
let width = qr.width();
let dark: Vec<bool> = qr.to_colors().into_iter().map(|color| color == qrcode::Color::Dark).collect();
// Whole pixels per module keep the edges crisp; about 160px in all.
let module = (160 / (width + 2 * QUIET)).max(2) as f32;
let rows = dark.chunks(width).map(|row| {
// Each run of dark modules is one div.
let mut runs: Vec<(bool, usize)> = Vec::new();
for &on in row {
match runs.last_mut() {
Some((last, count)) if *last == on => *count += 1,
_ => runs.push((on, 1)),
}
}
h_flex().h(px(module)).children(
runs.into_iter()
.map(|(on, count)| div().flex_none().h_full().w(px(module * count as f32)).when(on, |run| run.bg(black()))),
)
});
Some(
div()
.flex_none()
.p(px(8.))
.rounded(px(14.))
.raised(p)
.child(div().p(px(module * QUIET as f32)).rounded(px(6.)).bg(white()).children(rows)),
)
}