mod dialog;
mod types;
#[cfg(feature = "gui-win32")]
mod win32;
#[cfg(all(feature = "gui-gtk3", not(feature = "gui-win32")))]
mod gtk;
pub use dialog::{choose_language, confirm, error, info, warn};
pub use types::{
ButtonLabels, ConfiguredPage, CustomPageBuilder, CustomWidget, InstallCallback,
OnBeforeLeaveCallback, OnEnterCallback, WizardConfig, WizardPage,
};
#[doc(hidden)]
pub mod __private {
pub fn set_window_icon_png(bytes: &'static [u8]) {
#[cfg(all(feature = "gui-gtk3", not(feature = "gui-win32")))]
{
super::gtk::set_icon_bytes(bytes);
}
#[cfg(not(all(feature = "gui-gtk3", not(feature = "gui-win32"))))]
{
let _ = bytes;
}
}
}
use anyhow::Result;
use crate::Installer;
pub struct InstallerGui {
config: WizardConfig,
}
impl InstallerGui {
pub fn new(title: impl AsRef<str>) -> Self {
Self {
config: WizardConfig {
title: title.as_ref().to_string(),
pages: Vec::new(),
buttons: ButtonLabels::default(),
},
}
}
pub fn buttons(&mut self, labels: ButtonLabels) {
self.config.buttons = labels;
}
pub fn welcome(&mut self, title: impl AsRef<str>, message: impl AsRef<str>) -> PageHandle<'_> {
self.push_page(WizardPage::Welcome {
title: title.as_ref().to_string(),
message: message.as_ref().to_string(),
widgets: Vec::new(),
})
}
pub fn license(
&mut self,
heading: impl AsRef<str>,
text: impl AsRef<str>,
accept_label: impl AsRef<str>,
) -> PageHandle<'_> {
self.push_page(WizardPage::License {
heading: heading.as_ref().to_string(),
text: text.as_ref().to_string(),
accept_label: accept_label.as_ref().to_string(),
})
}
pub fn components_page(
&mut self,
heading: impl AsRef<str>,
label: impl AsRef<str>,
) -> PageHandle<'_> {
self.push_page(WizardPage::Components {
heading: heading.as_ref().to_string(),
label: label.as_ref().to_string(),
})
}
pub fn install_page(
&mut self,
callback: impl FnOnce(&mut Installer) -> Result<()> + Send + 'static,
) -> PageHandle<'_> {
self.push_page(WizardPage::Install {
callback: Box::new(callback),
is_uninstall: false,
show_log: true,
})
}
pub fn uninstall_page(
&mut self,
callback: impl FnOnce(&mut Installer) -> Result<()> + Send + 'static,
) -> PageHandle<'_> {
self.push_page(WizardPage::Install {
callback: Box::new(callback),
is_uninstall: true,
show_log: true,
})
}
pub fn finish_page(
&mut self,
title: impl AsRef<str>,
message: impl AsRef<str>,
) -> PageHandle<'_> {
self.push_page(WizardPage::Finish {
title: title.as_ref().to_string(),
message: message.as_ref().to_string(),
widgets: Vec::new(),
})
}
pub fn custom_page(
&mut self,
heading: impl AsRef<str>,
label: impl AsRef<str>,
build: impl FnOnce(&mut CustomPageBuilder),
) -> PageHandle<'_> {
let mut b = CustomPageBuilder::new();
build(&mut b);
self.push_page(WizardPage::Custom {
heading: heading.as_ref().to_string(),
label: label.as_ref().to_string(),
widgets: b.widgets,
})
}
pub fn error_page(
&mut self,
title: impl AsRef<str>,
message: impl AsRef<str>,
) -> PageHandle<'_> {
self.push_page(WizardPage::Error {
title: title.as_ref().to_string(),
message: message.as_ref().to_string(),
})
}
fn push_page(&mut self, page: WizardPage) -> PageHandle<'_> {
self.config.pages.push(ConfiguredPage::new(page));
PageHandle {
page: self.config.pages.last_mut().unwrap(),
}
}
pub fn run(self, installer: &mut Installer) -> Result<()> {
validate_widget_option_kinds(&self.config, installer);
if installer.headless {
self.run_headless(installer)
} else {
self.run_platform(installer)
}
}
fn run_headless(self, installer: &mut Installer) -> Result<()> {
let mut install_callback: Option<InstallCallback> = None;
for configured in self.config.pages {
if let WizardPage::Install { callback, .. } = configured.page {
install_callback = Some(callback);
}
}
if !installer.has_progress_sink() {
installer.set_progress_sink(Box::new(crate::StderrProgressSink::new()));
}
installer.reset_progress();
let result = if let Some(cb) = install_callback {
cb(installer)
} else {
Ok(())
};
if let Err(ref e) = result {
installer.log_error(e);
}
installer.clear_progress_sink();
result
}
#[cfg(feature = "gui-win32")]
fn run_platform(self, installer: &mut Installer) -> Result<()> {
win32::run_wizard(self.config, installer)
}
#[cfg(all(feature = "gui-gtk3", 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-gtk3"))))]
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-gtk3` on Linux."
))
}
}
pub struct PageHandle<'a> {
page: &'a mut ConfiguredPage,
}
impl<'a> PageHandle<'a> {
pub fn on_enter<F>(self, f: F) -> Self
where
F: Fn(&mut Installer) -> Result<()> + 'static,
{
self.page.on_enter = Some(Box::new(f));
self
}
pub fn on_before_leave<F>(self, f: F) -> Self
where
F: Fn(&mut Installer) -> Result<bool> + 'static,
{
self.page.on_before_leave = Some(Box::new(f));
self
}
pub fn skip_if<F>(self, f: F) -> Self
where
F: Fn(&Installer) -> bool + 'static,
{
self.page.skip_if = Some(Box::new(f));
self
}
pub fn hide_log(self) -> Self {
match &mut self.page.page {
WizardPage::Install { show_log, .. } => {
*show_log = false;
}
_ => panic!("hide_log is only supported on install / uninstall pages"),
}
self
}
pub fn with_widgets(self, build: impl FnOnce(&mut CustomPageBuilder)) -> Self {
let mut b = CustomPageBuilder::new();
build(&mut b);
match &mut self.page.page {
WizardPage::Welcome { widgets, .. } | WizardPage::Finish { widgets, .. } => {
widgets.extend(b.widgets);
}
_ => panic!(
"with_widgets is only supported on welcome and finish pages; \
use custom_page for other widget layouts"
),
}
self
}
}
fn validate_widget_option_kinds(config: &WizardConfig, installer: &Installer) {
use crate::OptionKind;
let widget_kinds = |widgets: &[CustomWidget]| -> Vec<(String, &'static str, OptionKind)> {
widgets
.iter()
.map(|w| match w {
CustomWidget::Text { key, .. } => (key.clone(), "text", OptionKind::String),
CustomWidget::Multiline { key, .. } => {
(key.clone(), "multiline", OptionKind::String)
}
CustomWidget::Number { key, .. } => (key.clone(), "number", OptionKind::Int),
CustomWidget::Checkbox { key, .. } => (key.clone(), "checkbox", OptionKind::Bool),
CustomWidget::Dropdown { key, .. } => (key.clone(), "dropdown", OptionKind::String),
CustomWidget::Radio { key, .. } => (key.clone(), "radio", OptionKind::String),
CustomWidget::FilePicker { key, .. } => {
(key.clone(), "file_picker", OptionKind::String)
}
CustomWidget::DirPicker { key, .. } => {
(key.clone(), "dir_picker", OptionKind::String)
}
})
.collect()
};
let kind_compatible = |widget: OptionKind, registered: OptionKind| match (widget, registered) {
(OptionKind::Bool, OptionKind::Bool | OptionKind::Flag) => true,
(a, b) => a == b,
};
for configured in &config.pages {
let widgets: &[CustomWidget] = match &configured.page {
WizardPage::Welcome { widgets, .. }
| WizardPage::Finish { widgets, .. }
| WizardPage::Custom { widgets, .. } => widgets,
_ => continue,
};
for (key, widget_name, expected) in widget_kinds(widgets) {
if let Some(registered) = installer.option_kind(&key) {
if !kind_compatible(expected, registered) {
panic!(
"wizard widget {widget_name:?} bound to option {key:?} expects \
OptionKind::{expected:?}, but it was registered as \
OptionKind::{registered:?}. Re-register with the matching kind \
or change the widget."
);
}
}
}
}
}