#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
const SETUP_HTML: &str = include_str!("setup.html");
#[cfg(target_os = "windows")]
mod platform {
use super::super::installer::{self, InstallProgress};
use super::SETUP_HTML;
use tao::event::{Event, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoopBuilder};
use tao::platform::run_return::EventLoopExtRunReturn;
use tao::window::WindowBuilder;
use wry::WebViewBuilder;
#[derive(Debug, Clone, PartialEq, Default)]
pub enum SetupResult {
Installed,
RunWithout,
#[default]
Cancelled,
}
#[derive(Debug)]
enum UiEvent {
Progress(InstallProgress),
Action(String),
}
pub fn show_setup_dialog() -> anyhow::Result<SetupResult> {
let mut event_loop = EventLoopBuilder::<UiEvent>::with_user_event().build();
let proxy = event_loop.create_proxy();
let window = WindowBuilder::new()
.with_title("Freenet Setup")
.with_inner_size(tao::dpi::LogicalSize::new(520.0, 440.0))
.with_resizable(false)
.with_decorations(true)
.build(&event_loop)
.map_err(|e| anyhow::anyhow!("Failed to create window: {e}"))?;
if let Some(monitor) = window.current_monitor() {
let screen = monitor.size();
let win_size = window.outer_size();
let x = (screen.width.saturating_sub(win_size.width)) / 2;
let y = (screen.height.saturating_sub(win_size.height)) / 2;
window.set_outer_position(tao::dpi::PhysicalPosition::new(x, y));
}
let webview2_data_dir = std::env::temp_dir().join("freenet-setup-webview2");
let mut web_context = wry::WebContext::new(Some(webview2_data_dir.clone()));
let ipc_proxy = proxy.clone();
let webview = WebViewBuilder::new_with_web_context(&mut web_context)
.with_html(SETUP_HTML)
.with_ipc_handler(move |msg| {
let _ = ipc_proxy.send_event(UiEvent::Action(msg.body().to_string()));
})
.with_devtools(false)
.build(&window)
.map_err(|e| anyhow::anyhow!("Failed to create WebView2: {e}"))?;
let mut result = SetupResult::Cancelled;
let mut installing = false;
let mut install_handle: Option<std::thread::JoinHandle<()>> = None;
event_loop.run_return(|event, _, control_flow| {
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => {
drop(install_handle.take());
*control_flow = ControlFlow::Exit;
}
Event::UserEvent(UiEvent::Action(action)) => match action.as_str() {
"install" => {
if installing {
return;
}
installing = true;
let install_proxy = proxy.clone();
let handle = std::thread::spawn(move || {
let cb_result = installer::run_install(|progress| {
let _ = install_proxy.send_event(UiEvent::Progress(progress));
});
if let Err(e) = cb_result {
let _ = install_proxy.send_event(UiEvent::Progress(
InstallProgress::Error(format!("{e:#}")),
));
}
});
install_handle = Some(handle);
}
"run_without" => {
result = SetupResult::RunWithout;
*control_flow = ControlFlow::Exit;
}
"close" => {
*control_flow = ControlFlow::Exit;
}
_ => {}
},
Event::UserEvent(UiEvent::Progress(progress)) => {
let js = match &progress {
InstallProgress::StoppingExisting => {
"updateProgress(7, 'Stopping existing service...')".to_string()
}
InstallProgress::CopyingBinary => {
"updateProgress(21, 'Copying files...')".to_string()
}
InstallProgress::DownloadingFdev => {
"updateProgress(35, 'Downloading developer tools...')".to_string()
}
InstallProgress::FdevSkipped(_) => {
"updateProgress(42, 'Developer tools skipped')".to_string()
}
InstallProgress::AddingToPath => {
"updateProgress(50, 'Configuring system PATH...')".to_string()
}
InstallProgress::InstallingService => {
"updateProgress(65, 'Installing background service...')".to_string()
}
InstallProgress::LaunchingService => {
"updateProgress(80, 'Starting Freenet...')".to_string()
}
InstallProgress::OpeningDashboard => {
"updateProgress(92, 'Opening dashboard...')".to_string()
}
InstallProgress::Complete => {
result = SetupResult::Installed;
"showComplete()".to_string()
}
InstallProgress::Error(msg) => {
let escaped = escape_for_js(msg);
format!("showError('{escaped}')")
}
};
let _ = webview.evaluate_script(&js);
}
_ => {}
}
});
drop(webview);
let _ = std::fs::remove_dir_all(&webview2_data_dir);
Ok(result)
}
fn escape_for_js(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('\n', "\\n")
.replace('\r', "\\r")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_for_js_special_chars() {
assert_eq!(escape_for_js("hello"), "hello");
assert_eq!(escape_for_js("it's"), "it\\'s");
assert_eq!(escape_for_js("line1\nline2"), "line1\\nline2");
assert_eq!(escape_for_js("path\\to\\file"), "path\\\\to\\\\file");
assert_eq!(
escape_for_js("err: can't\ndo it\\now"),
"err: can\\'t\\ndo it\\\\now"
);
}
}
}
#[cfg(not(target_os = "windows"))]
mod platform {
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)] pub enum SetupResult {
Installed,
RunWithout,
Cancelled,
}
#[allow(dead_code)] pub fn show_setup_dialog() -> anyhow::Result<SetupResult> {
Ok(SetupResult::RunWithout)
}
}
#[rustfmt::skip] #[cfg_attr(not(target_os = "windows"), allow(unused_imports))]
pub use platform::{SetupResult, show_setup_dialog};
#[cfg(test)]
mod html_tests {
use super::SETUP_HTML;
#[test]
fn test_setup_html_success_screen_has_visible_countdown() {
assert!(
SETUP_HTML.contains("id=\"countdown\""),
"the success screen must render a #countdown element",
);
assert!(
SETUP_HTML.contains("Closing in "),
"countdown text must say 'Closing in Ns...' so the user sees what is happening",
);
assert!(
SETUP_HTML.contains("setInterval("),
"countdown must use setInterval to update the visible label every second",
);
assert!(
SETUP_HTML.contains("Close now"),
"manual-close button label must read 'Close now' (not the old 'Close')",
);
assert!(
SETUP_HTML.contains("clearInterval("),
"doClose must clearInterval so a manual close cancels the pending tick",
);
}
}