use std::{
net::IpAddr,
time::{Duration, Instant},
};
use egui::{Color32, Context, DragValue, Grid, Id, RichText, Vec2, ViewportId};
use crate::remote::RemoteAccess;
use gled_ui::ui::window_common::{gled_window_frame, tool_window_builder};
const ADDRESS_REFRESH: Duration = Duration::from_secs(5);
#[derive(Debug, Default)]
pub struct RemoteAccessWindow {
open: bool,
addresses: Vec<IpAddr>,
addresses_read: Option<Instant>,
}
impl RemoteAccessWindow {
#[cfg_attr(feature = "profiling", profiling::function)]
pub fn update(&mut self, ctx: &Context, remote_access: &mut RemoteAccess) {
if !self.open {
return;
}
if self
.addresses_read
.is_none_or(|read| read.elapsed() > ADDRESS_REFRESH)
{
self.addresses = local_addresses();
self.addresses_read = Some(Instant::now());
}
ctx.show_viewport_immediate(
ViewportId(Id::new("remote access window")),
tool_window_builder("Remote access")
.with_title("Remote access")
.with_inner_size(Vec2::new(520.0, 420.0))
.with_min_inner_size(Vec2::new(420.0, 300.0)),
|ctx, _viewport_class| {
ctx.input(|input| {
if input.viewport().close_requested()
|| gled_ui::ui::window_common::embedded_close_clicked("Remote access")
{
self.close();
}
});
gled_window_frame(ctx, "Remote access", |ui| {
self.contents(ui, remote_access);
});
},
);
}
fn contents(&mut self, ui: &mut egui::Ui, remote_access: &mut RemoteAccess) {
let mut settings = remote_access.settings();
ui.checkbox(
&mut settings.enabled,
"Allow browsers and other devices to control gled",
);
ui.add_space(6.0);
Grid::new("remote access ports")
.num_columns(2)
.show(ui, |ui| {
ui.label("HTTPS port");
ui.add(DragValue::new(&mut settings.https_port).range(1..=65535));
ui.end_row();
let mut redirect = settings.http_redirect_port.is_some();
ui.checkbox(&mut redirect, "HTTP redirect port");
let mut port = settings.http_redirect_port.unwrap_or(8080);
ui.add_enabled(redirect, DragValue::new(&mut port).range(1..=65535));
settings.http_redirect_port = redirect.then_some(port);
ui.end_row();
});
remote_access.change(settings);
ui.separator();
if let Some(error) = remote_access.error() {
ui.label(RichText::new(error).color(Color32::RED));
return;
}
let (Some(port), Some(fingerprint)) =
(remote_access.https_port(), remote_access.fingerprint())
else {
ui.label("Off. Only this window controls gled.");
return;
};
ui.label("Open one of these addresses in a browser with WebGPU (Chrome, Edge, Safari):");
for url in urls(&self.addresses, port) {
ui.horizontal(|ui| {
if ui.small_button("Copy").clicked() {
ui.ctx().copy_text(url.clone());
}
ui.monospace(url);
});
}
ui.add_space(6.0);
ui.label(
"The browser warns about the certificate the first time, because gled made it \
itself. Accept it if the fingerprint matches:",
);
ui.add(egui::Label::new(RichText::new(fingerprint).monospace().small()).wrap());
ui.separator();
let clients: Vec<_> = remote_access.clients().cloned().collect();
if clients.is_empty() {
ui.label("Nobody is connected.");
return;
}
ui.label(format!("Connected: {}", clients.len()));
Grid::new("remote access clients")
.num_columns(3)
.striped(true)
.show(ui, |ui| {
for client in clients {
ui.monospace(client.peer.ip().to_string());
ui.label(client.app_version);
ui.label(format_since(client.since.elapsed()));
ui.end_row();
}
});
}
pub fn open(&mut self) {
self.open = true;
self.addresses_read = None;
}
pub fn close(&mut self) {
self.open = false;
}
}
fn local_addresses() -> Vec<IpAddr> {
let networks = sysinfo::Networks::new_with_refreshed_list();
let mut addresses: Vec<IpAddr> = networks
.values()
.flat_map(|network| network.ip_networks().iter().map(|ip| ip.addr))
.filter(|addr| match addr {
IpAddr::V4(addr) => !addr.is_loopback() && !addr.is_link_local(),
IpAddr::V6(addr) => {
!addr.is_loopback() && !addr.is_unicast_link_local() && !addr.is_multicast()
}
})
.collect();
addresses.sort();
addresses.dedup();
addresses
}
fn urls(addresses: &[IpAddr], port: u16) -> Vec<String> {
let port = if port == 443 {
String::new()
} else {
format!(":{port}")
};
let mut urls = Vec::new();
if let Ok(hostname) = gethostname::gethostname().into_string() {
let hostname = hostname.trim_end_matches(".local");
if !hostname.is_empty() {
urls.push(format!("https://{hostname}.local{port}"));
}
}
for address in addresses {
match address {
IpAddr::V4(address) => urls.push(format!("https://{address}{port}")),
IpAddr::V6(address) => urls.push(format!("https://[{address}]{port}")),
}
}
urls
}
fn format_since(elapsed: Duration) -> String {
let seconds = elapsed.as_secs();
if seconds < 60 {
format!("{seconds} s")
} else if seconds < 3600 {
format!("{} min", seconds / 60)
} else {
format!("{} h {} min", seconds / 3600, seconds / 60 % 60)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn urls_leave_out_the_default_port() {
let addresses = [
"192.168.1.5".parse().expect("ip"),
"fd00::5".parse().expect("ip"),
];
let urls = urls(&addresses, 443);
assert!(urls.contains(&"https://192.168.1.5".to_owned()));
assert!(urls.contains(&"https://[fd00::5]".to_owned()));
let urls = super::urls(&addresses, 8443);
assert!(urls.contains(&"https://192.168.1.5:8443".to_owned()));
}
}