use std::net::{IpAddr, SocketAddr};
use std::time::Instant;
use a7n_ipc::{DEFAULT_UDP_PORT, MachineId, PairingCode, PeerInfo, Request, StatusInfo};
use eframe::egui;
use rust_i18n::t;
use super::{address_text, firewall::firewall_warnings};
use crate::{App, Command, theme};
use theme::{BadgeKind, ButtonKind, ButtonSize, IconButtonKind, icon};
impl App {
pub(super) fn machines_header(&mut self, ui: &mut egui::Ui) {
let Some(status) = self.status.clone() else {
return;
};
theme::page_header(
ui,
&t!("Machines"),
&t!("Share input between these machines."),
|ui| {
if theme::button(
ui,
ButtonKind::Secondary,
ButtonSize::Md,
Some(if status.paused {
icon::PLAY
} else {
icon::PAUSE
}),
&if status.paused {
t!("Resume")
} else {
t!("Pause")
},
)
.clicked()
{
self.send(Command::Request(Request::SetPaused {
paused: !status.paused,
}));
}
},
);
}
pub(crate) fn machines(&mut self, ui: &mut egui::Ui) {
let Some(status) = self.status.clone() else {
return;
};
self.page_message(ui);
firewall_warnings(ui, &status.firewall, self.hide_addresses());
self.held_files(ui, &status);
self.active_transfers(ui, &status);
theme::overline(ui, &t!("This machine"));
theme::card(ui, |ui| {
machine_identity(
ui,
&status.name,
status.machine,
BadgeKind::Accent,
&t!("This machine"),
);
if let Some(reason) = &status.input {
theme::error(ui, &t!("Input unavailable: %{reason}", reason = reason));
}
});
ui.add_space(theme::size::SECTION_GAP);
self.paired_machines(ui, &status);
ui.add_space(theme::size::SECTION_GAP);
self.nearby_machines(ui, &status);
ui.add_space(theme::size::SECTION_GAP);
self.pairing_ui(ui);
}
fn paired_machines(&mut self, ui: &mut egui::Ui, status: &StatusInfo) {
let peers: Vec<_> = status
.peers
.iter()
.filter(|peer| peer.id != status.machine)
.collect();
theme::overline(
ui,
&t!(
"Paired machines · %{count}",
count = &peers.len().to_string()
),
);
if peers.is_empty() {
theme::empty_card(
ui,
icon::MONITOR,
&t!("No paired machines."),
&t!("Choose Host or Join below."),
);
}
for peer in peers {
self.paired_machine(ui, status, peer);
}
}
fn paired_machine(&mut self, ui: &mut egui::Ui, status: &StatusInfo, peer: &PeerInfo) {
theme::card(ui, |ui| {
machine_identity(
ui,
&peer.name,
peer.id,
if peer.waking {
BadgeKind::Warning
} else if peer.connected {
BadgeKind::Online
} else {
BadgeKind::Offline
},
&if peer.waking {
t!("Waking…")
} else if peer.connected {
t!("Online")
} else {
t!("Offline")
},
);
super::peers::peer_problem(ui, peer);
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if theme::button(
ui,
ButtonKind::DangerQuiet,
ButtonSize::Md,
Some(icon::X_CIRCLE),
&t!("Unpair"),
)
.clicked()
{
self.send(Command::Request(Request::Unpair { machine: peer.id }));
}
if !peer.connected && peer.can_wake {
ui.add_enabled_ui(!peer.waking, |ui| {
if theme::button(
ui,
ButtonKind::Secondary,
ButtonSize::Md,
Some(icon::POWER),
&t!("Wake"),
)
.clicked()
{
self.send(Command::Request(Request::Wake { machine: peer.id }));
}
});
}
let nearby = status.nearby.iter().find(|nearby| nearby.id == peer.id);
ui.add_enabled_ui(!peer.connected && nearby.is_some(), |ui| {
if theme::button(
ui,
ButtonKind::Secondary,
ButtonSize::Md,
None,
&t!("Connect"),
)
.on_hover_text(t!("Connect",))
.clicked()
&& let Some(nearby) = nearby
{
self.send(Command::Request(Request::Connect { addr: nearby.addr }));
}
});
});
});
});
}
fn nearby_machines(&mut self, ui: &mut egui::Ui, status: &StatusInfo) {
theme::overline(ui, &t!("Nearby machines"));
let nearby: Vec<_> = status
.nearby
.iter()
.filter(|nearby| {
nearby.id != status.machine && !status.peers.iter().any(|peer| peer.id == nearby.id)
})
.collect();
if nearby.is_empty() {
theme::empty_card(
ui,
icon::BROADCAST,
&t!("No nearby machines found."),
&t!("Start Apeirogon on the same subnet."),
);
}
for nearby in nearby {
theme::card(ui, |ui| {
ui.horizontal(|ui| {
theme::icon_tile(ui, icon::MONITOR);
ui.vertical(|ui| {
ui.label(t!("Nearby machine"));
machine_id(ui, nearby.id);
ui.add(
egui::Label::new(
egui::RichText::new(address_text(
&nearby.addr,
self.hide_addresses(),
))
.font(theme::font_mono(13.0)),
)
.truncate(),
);
});
});
if theme::button(ui, ButtonKind::Secondary, ButtonSize::Md, None, &t!("Pair"))
.clicked()
{
self.address = nearby.addr.to_string();
}
});
}
}
fn pairing_ui(&mut self, ui: &mut egui::Ui) {
theme::overline(ui, &t!("Pair a machine"));
let count = if ui.available_width() < theme::STACK_WIDTH {
1
} else {
2
};
ui.columns(count, |columns| {
theme::card(&mut columns[0], |ui| {
ui.heading(t!("Host"));
ui.label(t!("Enter this one-time code on the other machine.",));
if let Some((code, expiry)) = &self.pairing {
let remaining = expiry.saturating_duration_since(Instant::now()).as_secs();
if remaining == 0 {
theme::small(ui, &t!("Pairing code expired"));
} else {
ui.label(egui::RichText::new(code).font(theme::font_mono(13.0)));
theme::small(ui, &t!("Expires in %{remaining} s", remaining = remaining));
}
}
if theme::button(
ui,
ButtonKind::Primary,
ButtonSize::Md,
Some(icon::BROADCAST),
&t!("Host"),
)
.clicked()
{
self.send(Command::Request(Request::PairHost));
}
});
theme::card(&mut columns[count - 1], |ui| self.join_pairing(ui));
});
}
fn join_pairing(&mut self, ui: &mut egui::Ui) {
ui.heading(t!("Join"));
ui.label(t!("Enter the host's address and code."));
let hidden = self.hide_addresses();
theme::masked_text_input(
ui,
&t!("Address"),
&mut self.address,
&t!("IP or IP:port"),
true,
hidden,
);
theme::text_input(
ui,
&t!("Pairing code"),
&mut self.code,
&t!("Six digits"),
true,
);
let address = parse_address(&self.address);
let code = PairingCode::parse(self.code.trim());
if !self.address.is_empty() && address.is_err() {
theme::error(ui, &t!("Enter an IP or IP:port; use [IPv6]:port"));
}
if !self.code.is_empty() && code.is_err() {
theme::error(ui, &t!("Code must be six ASCII digits"));
}
ui.add_enabled_ui(address.is_ok() && code.is_ok(), |ui| {
if theme::button(ui, ButtonKind::Secondary, ButtonSize::Md, None, &t!("Join")).clicked()
&& let (Ok(addr), Ok(code)) = (address, code)
{
self.send(Command::Request(Request::PairJoin { addr, code }));
self.code.clear();
}
});
ui.add_enabled_ui(address.is_ok(), |ui| {
if theme::button(ui, ButtonKind::Ghost, ButtonSize::Sm, None, &t!("Connect")).clicked()
&& let Ok(addr) = address
{
self.send(Command::Request(Request::Connect { addr }));
}
});
}
}
fn machine_identity(ui: &mut egui::Ui, name: &str, id: MachineId, kind: BadgeKind, status: &str) {
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
copy_id(ui, id);
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
theme::icon_tile(ui, icon::MONITOR);
ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = 2.0;
ui.spacing_mut().interact_size.y = 22.0;
ui.horizontal_wrapped(|ui| {
ui.add(
egui::Label::new(
egui::RichText::new(name)
.font(theme::font_medium(14.0))
.color(theme::Palette::of(ui.ctx()).text),
)
.truncate(),
);
theme::badge(ui, kind, status);
});
id_label(ui, id);
});
});
});
});
}
fn machine_id(ui: &mut egui::Ui, id: MachineId) {
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
copy_id(ui, id);
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
id_label(ui, id);
});
});
});
}
fn id_label(ui: &mut egui::Ui, id: MachineId) {
ui.add(
egui::Label::new(
egui::RichText::new(id.to_string())
.font(theme::font_mono(12.0))
.color(theme::Palette::of(ui.ctx()).text3),
)
.truncate(),
);
}
fn copy_id(ui: &mut egui::Ui, id: MachineId) {
if theme::icon_button(ui, IconButtonKind::Ghost, icon::COPY, &t!("Copy ID"), false).clicked() {
ui.ctx().copy_text(id.to_string());
}
}
fn parse_address(text: &str) -> Result<SocketAddr, ()> {
text.trim()
.parse()
.or_else(|_| {
text.trim()
.parse::<IpAddr>()
.map(|ip| SocketAddr::new(ip, DEFAULT_UDP_PORT))
})
.map_err(|_| ())
}