pub mod dialog;
mod types;
#[cfg(feature = "gui-win32")]
mod win32;
#[cfg(all(feature = "gui-gtk", not(feature = "gui-win32")))]
mod gtk;
pub use dialog::{choose_language, confirm, error, info, warn};
#[doc(hidden)]
pub fn __set_window_icon_png(bytes: &'static [u8]) {
#[cfg(all(feature = "gui-gtk", not(feature = "gui-win32")))]
{
gtk::set_icon_bytes(bytes);
}
#[cfg(not(all(feature = "gui-gtk", not(feature = "gui-win32"))))]
{
let _ = bytes;
}
}
pub use types::{
ButtonLabels, ConfiguredPage, CustomPageBuilder, CustomWidget, ExitCallback, GuiContext,
GuiMessage, InstallCallback, OnBeforeLeaveCallback, OnEnterCallback, PageContext,
StartCallback, WizardConfig, WizardPage,
};
use anyhow::Result;
use crate::{Installer, ProgressSink};
pub struct InstallerGui {
config: WizardConfig,
}
impl InstallerGui {
pub fn wizard() -> Self {
Self {
config: WizardConfig {
title: "Installer".to_string(),
pages: Vec::new(),
buttons: ButtonLabels::default(),
on_start: None,
on_exit: None,
},
}
}
pub fn on_start(mut self, f: impl FnOnce(&mut Installer) -> Result<()> + 'static) -> Self {
self.config.on_start = Some(Box::new(f));
self
}
pub fn on_exit(mut self, f: impl FnOnce(&mut Installer) -> Result<()> + 'static) -> Self {
self.config.on_exit = Some(Box::new(f));
self
}
pub fn title(mut self, title: &str) -> Self {
self.config.title = title.to_string();
self
}
pub fn buttons(mut self, labels: ButtonLabels) -> Self {
self.config.buttons = labels;
self
}
pub fn welcome(mut self, title: &str, message: &str) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Welcome {
title: title.to_string(),
message: message.to_string(),
}));
self
}
pub fn license(mut self, heading: &str, text: &str, accept_label: &str) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::License {
heading: heading.to_string(),
text: text.to_string(),
accept_label: accept_label.to_string(),
}));
self
}
pub fn components_page(mut self, heading: &str, label: &str) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Components {
heading: heading.to_string(),
label: label.to_string(),
}));
self
}
pub fn directory_picker(mut self, heading: &str, label: &str, default: &str) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::DirectoryPicker {
heading: heading.to_string(),
label: label.to_string(),
default: default.to_string(),
}));
self
}
pub fn install_page(
mut self,
callback: impl FnOnce(&mut GuiContext) -> Result<()> + Send + 'static,
) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Install {
callback: Box::new(callback),
is_uninstall: false,
}));
self
}
pub fn uninstall_page(
mut self,
callback: impl FnOnce(&mut GuiContext) -> Result<()> + Send + 'static,
) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Install {
callback: Box::new(callback),
is_uninstall: true,
}));
self
}
pub fn finish_page(mut self, title: &str, message: &str) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Finish {
title: title.to_string(),
message: message.to_string(),
}));
self
}
pub fn custom_page(
mut self,
heading: &str,
label: &str,
build: impl FnOnce(&mut CustomPageBuilder),
) -> Self {
let mut b = CustomPageBuilder::new();
build(&mut b);
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Custom {
heading: heading.to_string(),
label: label.to_string(),
widgets: b.widgets,
}));
self
}
pub fn error_page(mut self, title: &str, message: &str) -> Self {
self.config
.pages
.push(ConfiguredPage::new(WizardPage::Error {
title: title.to_string(),
message: message.to_string(),
}));
self
}
pub fn on_enter<F>(mut self, f: F) -> Self
where
F: Fn(&mut PageContext) -> Result<()> + 'static,
{
if let Some(last) = self.config.pages.last_mut() {
last.on_enter = Some(Box::new(f));
}
self
}
pub fn on_before_leave<F>(mut self, f: F) -> Self
where
F: Fn(&mut PageContext) -> Result<bool> + 'static,
{
if let Some(last) = self.config.pages.last_mut() {
last.on_before_leave = Some(Box::new(f));
}
self
}
pub fn skip_if<F>(mut self, f: F) -> Self
where
F: Fn(&PageContext) -> bool + 'static,
{
if let Some(last) = self.config.pages.last_mut() {
last.skip_if = Some(Box::new(f));
}
self
}
pub fn run(mut self, installer: &mut Installer) -> Result<()> {
let on_start = self.config.on_start.take();
let on_exit = self.config.on_exit.take();
if let Some(cb) = on_start {
cb(installer)?;
}
let result = if installer.headless {
self.run_headless(installer)
} else {
self.run_platform(installer)
};
if let Some(cb) = on_exit {
if let Err(e) = cb(installer) {
eprintln!("on_exit error: {e:#}");
}
}
result
}
fn run_headless(self, installer: &mut Installer) -> Result<()> {
use std::sync::{mpsc, Arc, Mutex};
let mut install_callback: Option<InstallCallback> = None;
let mut default_dir = String::new();
for configured in self.config.pages {
match configured.page {
WizardPage::Install { callback, .. } => install_callback = Some(callback),
WizardPage::DirectoryPicker { default, .. } if default_dir.is_empty() => {
default_dir = default;
}
_ => {}
}
}
let cancelled = installer.cancellation_flag();
let installer_taken = std::mem::replace(installer, Installer::new(&[], &[], "none"));
let installer_arc = Arc::new(Mutex::new(installer_taken));
let install_dir = Arc::new(Mutex::new(default_dir));
let (tx, rx) = mpsc::channel::<GuiMessage>();
let drainer = std::thread::spawn(move || {
for msg in rx {
match msg {
GuiMessage::SetStatus(s) => eprintln!("[*] {s}"),
GuiMessage::Log(m) => eprintln!(" {m}"),
GuiMessage::SetProgress(_) | GuiMessage::Finished(_) => {}
}
}
});
struct HeadlessSink {
tx: mpsc::Sender<GuiMessage>,
}
impl ProgressSink for HeadlessSink {
fn set_status(&self, s: &str) {
let _ = self.tx.send(GuiMessage::SetStatus(s.to_string()));
}
fn set_progress(&self, _: f64) {}
fn log(&self, m: &str) {
let _ = self.tx.send(GuiMessage::Log(m.to_string()));
}
}
{
let mut inst = installer_arc.lock().unwrap();
inst.set_progress_sink(Box::new(HeadlessSink { tx: tx.clone() }));
inst.reset_progress();
}
let result = (|| -> Result<()> {
if let Some(cb) = install_callback {
let mut ctx = GuiContext::new(
tx.clone(),
installer_arc.clone(),
install_dir.clone(),
cancelled.clone(),
);
cb(&mut ctx)?;
}
Ok(())
})();
if let Err(ref e) = result {
installer_arc.lock().unwrap().log_error(e);
}
installer_arc.lock().unwrap().clear_progress_sink();
drop(tx);
let _ = drainer.join();
let restored = Arc::try_unwrap(installer_arc)
.map_err(|_| anyhow::anyhow!("installer still referenced after headless run"))?
.into_inner()
.map_err(|e| anyhow::anyhow!("installer mutex poisoned: {e}"))?;
*installer = restored;
result
}
#[cfg(feature = "gui-win32")]
fn run_platform(self, installer: &mut Installer) -> Result<()> {
win32::run_wizard(self.config, installer)
}
#[cfg(all(feature = "gui-gtk", not(feature = "gui-win32")))]
fn run_platform(self, installer: &mut Installer) -> Result<()> {
gtk::run_wizard(self.config, installer)
}
#[cfg(all(feature = "gui", not(any(feature = "gui-win32", feature = "gui-gtk"))))]
fn run_platform(self, _installer: &mut Installer) -> Result<()> {
Err(anyhow::anyhow!(
"No GUI backend available for this platform. Enable `gui-win32` on Windows or `gui-gtk` on Linux."
))
}
}