]> git.proxmox.com Git - pve-installer.git/blobdiff - proxmox-tui-installer/src/main.rs
tui: install progress: use ok/cancel as button text for installer prompt
[pve-installer.git] / proxmox-tui-installer / src / main.rs
index e39325b9cc31621fa8ca53bbe10ce4ca220cf315..4c1448248575facea743bd8725e76f69277501da 100644 (file)
@@ -1,56 +1,49 @@
 #![forbid(unsafe_code)]
 
-mod options;
-mod setup;
-mod system;
-mod utils;
-mod views;
+use std::{collections::HashMap, env, net::IpAddr};
 
-use crate::options::*;
 use cursive::{
     event::Event,
-    view::{Nameable, Resizable, ViewWrapper},
+    theme::{ColorStyle, Effect, PaletteColor, Style},
+    view::{Nameable, Offset, Resizable, ViewWrapper},
     views::{
-        Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, PaddedView, Panel,
-        ProgressBar, ResizedView, ScrollView, SelectView, TextContent, TextView, ViewRef,
+        Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
+        ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
     },
-    Cursive, CursiveRunnable, ScreenId, View,
+    Cursive, CursiveRunnable, ScreenId, View, XY,
 };
-use setup::{LocaleInfo, ProxmoxProduct, SetupInfo};
-use std::{collections::HashMap, env, net::IpAddr};
-use utils::Fqdn;
+
+use regex::Regex;
+
+mod options;
+use options::InstallerOptions;
+
+use proxmox_installer_common::{
+    options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
+    setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
+    utils::Fqdn,
+};
+
+mod setup;
+
+mod system;
+
+mod views;
 use views::{
-    BootdiskOptionsView, CidrAddressEditView, FormView, TableView, TableViewItem,
-    TimezoneOptionsView,
+    BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView,
+    TableViewItem, TimezoneOptionsView,
 };
 
 // TextView::center() seems to garble the first two lines, so fix it manually here.
-const LOGO_PVE: &str = r#"
-       ____                                          _    __ _____
-      / __ \_________  _  ______ ___  ____  _  __   | |  / / ____/
-  / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/   | | / / __/
- / ____/ /  / /_/ />  </ / / / / / /_/ />  <     | |/ / /___
-/_/   /_/   \____/_/|_/_/ /_/ /_/\____/_/|_|     |___/_____/
-"#;
-
-const LOGO_PBS: &str = r#"
-      ____                                          ____ _____
-     / __ \_________  _  ______ ___  ____  _  __   / __ ) ___/
-   / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/  / __  \__ \
-  / ____/ /  / /_/ />  </ / / / / / /_/ />  <   / /_/ /__/ /
-/_/   /_/   \____/_/|_/_/ /_/ /_/\____/_/|_|  /_____/____/
-"#;
-
-const LOGO_PMG: &str = r#"
-       ____                                          __  _________
-      / __ \_________  _  ______ ___  ____  _  __   /  |/  / ____/
-   / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/  / /|_/ / / __
-  / ____/ /  / /_/ />  </ / / / / / /_/ />  <   / /  / / /_/ /
-/_/   /_/   \____/_/|_/_/ /_/ /_/\____/_/|_|  /_/  /_/\____/
-"#;
+const PROXMOX_LOGO: &str = r#"
+ ____
+|  _ \ _ __ _____  ___ __ ___   _____  __
+| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
+|  __/| | | (_) >  <| | | | | | (_) >  <
+|_|   |_|  \___/_/\_\_| |_| |_|\___/_/\_\ "#;
 
 struct InstallerView {
-    view: ResizedView<LinearLayout>,
+    view: ResizedView<Dialog>,
 }
 
 impl InstallerView {
@@ -58,21 +51,21 @@ impl InstallerView {
         state: &InstallerState,
         view: T,
         next_cb: Box<dyn Fn(&mut Cursive)>,
+        focus_next: bool,
     ) -> Self {
-        let inner = LinearLayout::vertical()
+        let mut bbar = LinearLayout::horizontal()
+            .child(abort_install_button())
+            .child(DummyView.full_width())
+            .child(Button::new("Previous", switch_to_prev_screen))
+            .child(DummyView)
+            .child(Button::new("Next", next_cb));
+        let _ = bbar.set_focus_index(4); // ignore errors
+        let mut inner = LinearLayout::vertical()
             .child(PaddedView::lrtb(0, 0, 1, 1, view))
-            .child(PaddedView::lrtb(
-                1,
-                1,
-                0,
-                0,
-                LinearLayout::horizontal()
-                    .child(abort_install_button())
-                    .child(DummyView.full_width())
-                    .child(Button::new("Previous", switch_to_prev_screen))
-                    .child(DummyView)
-                    .child(Button::new("Next", next_cb)),
-            ));
+            .child(PaddedView::lrtb(1, 1, 0, 0, bbar));
+        if focus_next {
+            let _ = inner.set_focus_index(1); // ignore errors
+        }
 
         Self::with_raw(state, inner)
     }
@@ -80,20 +73,12 @@ impl InstallerView {
     pub fn with_raw(state: &InstallerState, view: impl View) -> Self {
         let setup = &state.setup_info;
 
-        let logo = match setup.config.product {
-            ProxmoxProduct::PVE => LOGO_PVE,
-            ProxmoxProduct::PBS => LOGO_PBS,
-            ProxmoxProduct::PMG => LOGO_PMG,
-        };
-
         let title = format!(
             "{} ({}-{}) Installer",
             setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease
         );
 
-        let inner = LinearLayout::vertical()
-            .child(PaddedView::lrtb(1, 1, 0, 1, TextView::new(logo).center()))
-            .child(Dialog::around(view).title(title));
+        let inner = Dialog::around(view).title(title);
 
         Self {
             // Limit the maximum to something reasonable, such that it won't get spread out much
@@ -104,7 +89,41 @@ impl InstallerView {
 }
 
 impl ViewWrapper for InstallerView {
-    cursive::wrap_impl!(self.view: ResizedView<LinearLayout>);
+    cursive::wrap_impl!(self.view: ResizedView<Dialog>);
+}
+
+struct InstallerBackgroundView {
+    view: StackView,
+}
+
+impl InstallerBackgroundView {
+    pub fn new() -> Self {
+        let style = Style {
+            effects: Effect::Bold.into(),
+            color: ColorStyle::back(PaletteColor::View),
+        };
+
+        let mut view = StackView::new();
+        view.add_fullscreen_layer(Layer::with_color(
+            DummyView
+                .full_width()
+                .fixed_height(PROXMOX_LOGO.lines().count() + 1),
+            ColorStyle::back(PaletteColor::View),
+        ));
+        view.add_transparent_layer_at(
+            XY {
+                x: Offset::Center,
+                y: Offset::Absolute(0),
+            },
+            TextView::new(PROXMOX_LOGO).style(style),
+        );
+
+        Self { view }
+    }
+}
+
+impl ViewWrapper for InstallerBackgroundView {
+    cursive::wrap_impl!(self.view: StackView);
 }
 
 #[derive(Clone, Eq, Hash, PartialEq)]
@@ -121,8 +140,8 @@ enum InstallerStep {
 #[derive(Clone)]
 struct InstallerState {
     options: InstallerOptions,
-    available_disks: Vec<Disk>,
     setup_info: SetupInfo,
+    runtime_info: RuntimeInfo,
     locales: LocaleInfo,
     steps: HashMap<InstallerStep, ScreenId>,
     in_test_mode: bool,
@@ -135,14 +154,10 @@ fn main() {
         Some("-t") => true,
 
         // Always force the test directory in debug builds
-        #[cfg(debug_assertions)]
-        _ => true,
-
-        #[cfg(not(debug_assertions))]
-        _ => false,
+        _ => cfg!(debug_assertions),
     };
 
-    let (setup_info, locales) = match installer_setup(in_test_mode) {
+    let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) {
         Ok(result) => result,
         Err(err) => initial_setup_error(&mut siv, &err),
     };
@@ -150,22 +165,16 @@ fn main() {
     siv.clear_global_callbacks(Event::CtrlChar('c'));
     siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog);
 
-    // TODO: retrieve actual disk info
-    let available_disks = vec![Disk {
-        path: "/dev/vda".to_owned(),
-        size: 17179869184,
-    }];
-
     siv.set_user_data(InstallerState {
         options: InstallerOptions {
-            bootdisk: BootdiskOptions::defaults_from(&available_disks[0]),
-            timezone: TimezoneOptions::default(),
-            password: PasswordOptions::default(),
-            network: NetworkOptions::default(),
-            reboot: false,
+            bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
+            timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
+            password: Default::default(),
+            network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
+            autoreboot: false,
         },
-        available_disks,
         setup_info,
+        runtime_info,
         locales,
         steps: HashMap::new(),
         in_test_mode,
@@ -175,42 +184,38 @@ fn main() {
     siv.run();
 }
 
-fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo), String> {
-    system::has_min_requirements()?;
-
-    let testdir = || {
-        env::current_dir()
-            .map(|mut p| {
-                p.push("testdir");
-                p
-            })
-            .map_err(|err| err.to_string())
-    };
-
-    let mut path = if in_test_mode {
-        testdir()?
-    } else {
-        std::path::PathBuf::from("/")
-    };
-
-    path.push("run");
-    path.push("proxmox-installer");
-
-    let installer_info = {
-        let mut path = path.clone();
-        path.push("iso-info.json");
-
-        setup::read_json(&path).map_err(|err| format!("Failed to retrieve setup info: {err}"))?
-    };
+/// Anything that can be done late in the setup and will not result in fatal errors.
+fn installer_setup_late(siv: &mut Cursive) {
+    let state = siv.user_data::<InstallerState>().cloned().unwrap();
 
-    let locale_info = {
-        let mut path = path.clone();
-        path.push("locales.json");
+    if !state.in_test_mode {
+        let kmap_id = &state.options.timezone.kb_layout;
+        if let Some(kmap) = state.locales.kmap.get(kmap_id) {
+            if let Err(err) = system::set_keyboard_layout(kmap) {
+                display_setup_warning(siv, &format!("Failed to apply keyboard layout: {err}"));
+            }
+        }
+    }
 
-        setup::read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))?
-    };
+    if state.runtime_info.total_memory < 1024 {
+        display_setup_warning(
+            siv,
+            concat!(
+                "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
+                "See 'System Requirements' in the documentation."
+            ),
+        );
+    }
 
-    Ok((installer_info, locale_info))
+    if state.setup_info.config.product == ProxmoxProduct::PVE && !state.runtime_info.hvm_supported {
+        display_setup_warning(
+            siv,
+            concat!(
+                "No support for hardware-accelerated KVM virtualization detected.\n\n",
+                "Check BIOS settings for Intel VT / AMD-V / SVM."
+            ),
+        );
+    }
 }
 
 fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
@@ -224,23 +229,55 @@ fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
     std::process::exit(1);
 }
 
+fn display_setup_warning(siv: &mut Cursive, message: &str) {
+    siv.add_layer(Dialog::info(message).title("Warning"));
+}
+
 fn switch_to_next_screen(
     siv: &mut Cursive,
     step: InstallerStep,
     constructor: &dyn Fn(&mut Cursive) -> InstallerView,
 ) {
+    let state = siv.user_data::<InstallerState>().cloned().unwrap();
+    let is_first_screen = state.steps.is_empty();
+
     // Check if the screen already exists; if yes, then simply switch to it.
-    if let Some(state) = siv.user_data::<InstallerState>().cloned() {
-        if let Some(screen_id) = state.steps.get(&step) {
-            siv.set_screen(*screen_id);
-            return;
+    if let Some(screen_id) = state.steps.get(&step) {
+        siv.set_screen(*screen_id);
+
+        // The summary view cannot be cached (otherwise it would display stale values). Thus
+        // replace it if the screen is switched to.
+        // TODO: Could be done by e.g. having all the main dialog views implement some sort of
+        // .refresh(), which can be called if the view is switched to.
+        if step == InstallerStep::Summary {
+            let view = constructor(siv);
+            siv.screen_mut().pop_layer();
+            siv.screen_mut().add_layer(view);
         }
+
+        return;
     }
 
     let v = constructor(siv);
     let screen = siv.add_active_screen();
     siv.with_user_data(|state: &mut InstallerState| state.steps.insert(step, screen));
+
+    siv.screen_mut().add_transparent_layer_at(
+        XY {
+            x: Offset::Parent(0),
+            y: Offset::Parent(0),
+        },
+        InstallerBackgroundView::new(),
+    );
+
     siv.screen_mut().add_layer(v);
+
+    // If this is the first screen to be added, execute our late setup first.
+    // Needs to be done here, at the end, to ensure that any potential layers get added to
+    // the right screen and are on top.
+    if is_first_screen {
+        installer_setup_late(siv);
+    }
 }
 
 fn switch_to_prev_screen(siv: &mut Cursive) {
@@ -248,18 +285,26 @@ fn switch_to_prev_screen(siv: &mut Cursive) {
     siv.set_screen(id);
 }
 
-#[cfg(not(debug_assertions))]
-fn yes_no_dialog(
+fn prompt_dialog(
     siv: &mut Cursive,
     title: &str,
     text: &str,
-    callback: &'static dyn Fn(&mut Cursive),
+    yes_text: &str,
+    callback_yes: Box<dyn Fn(&mut Cursive)>,
+    no_text: &str,
+    callback_no: Box<dyn Fn(&mut Cursive)>,
 ) {
     siv.add_layer(
         Dialog::around(TextView::new(text))
             .title(title)
-            .dismiss_button("No")
-            .button("Yes", callback),
+            .button(no_text, move |siv| {
+                siv.pop_layer();
+                callback_no(siv);
+            })
+            .button(yes_text, move |siv| {
+                siv.pop_layer();
+                callback_yes(siv);
+            }),
     )
 }
 
@@ -268,11 +313,14 @@ fn trigger_abort_install_dialog(siv: &mut Cursive) {
     siv.quit();
 
     #[cfg(not(debug_assertions))]
-    yes_no_dialog(
+    prompt_dialog(
         siv,
         "Abort installation?",
         "Are you sure you want to abort the installation?",
-        &Cursive::quit,
+        "Yes",
+        Box::new(Cursive::quit),
+        "No",
+        Box::new(|_| {}),
     )
 }
 
@@ -280,16 +328,26 @@ fn abort_install_button() -> Button {
     Button::new("Abort", trigger_abort_install_dialog)
 }
 
-fn get_eula() -> String {
-    // TODO: properly using info from Proxmox::Install::Env::setup()
-    std::fs::read_to_string("/cdrom/EULA")
+fn get_eula(setup: &SetupInfo) -> String {
+    let mut path = setup.locations.iso.clone();
+    path.push("EULA");
+
+    std::fs::read_to_string(path)
         .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
 }
 
 fn license_dialog(siv: &mut Cursive) -> InstallerView {
     let state = siv.user_data::<InstallerState>().unwrap();
 
-    let inner = LinearLayout::vertical()
+    let mut bbar = LinearLayout::horizontal()
+        .child(abort_install_button())
+        .child(DummyView.full_width())
+        .child(Button::new("I agree", |siv| {
+            switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
+        }));
+    let _ = bbar.set_focus_index(2); // ignore errors
+
+    let mut inner = LinearLayout::vertical()
         .child(PaddedView::lrtb(
             0,
             0,
@@ -298,20 +356,11 @@ fn license_dialog(siv: &mut Cursive) -> InstallerView {
             TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
         ))
         .child(Panel::new(ScrollView::new(
-            TextView::new(get_eula()).center(),
+            TextView::new(get_eula(&state.setup_info)).center(),
         )))
-        .child(PaddedView::lrtb(
-            1,
-            1,
-            1,
-            0,
-            LinearLayout::horizontal()
-                .child(abort_install_button())
-                .child(DummyView.full_width())
-                .child(Button::new("I agree", |siv| {
-                    switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
-                })),
-        ));
+        .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
+
+    let _ = inner.set_focus_index(2); // ignore errors
 
     InstallerView::with_raw(state, inner)
 }
@@ -321,23 +370,25 @@ fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
 
     InstallerView::new(
         &state,
-        BootdiskOptionsView::new(&state.available_disks, &state.options.bootdisk)
+        BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk)
             .with_name("bootdisk-options"),
         Box::new(|siv| {
-            let options = siv
-                .call_on_name("bootdisk-options", BootdiskOptionsView::get_values)
-                .flatten();
-
-            if let Some(options) = options {
-                siv.with_user_data(|state: &mut InstallerState| {
-                    state.options.bootdisk = options;
-                });
-
-                switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
-            } else {
-                siv.add_layer(Dialog::info("Invalid values"));
+            let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
+
+            match options {
+                Some(Ok(options)) => {
+                    siv.with_user_data(|state: &mut InstallerState| {
+                        state.options.bootdisk = options;
+                    });
+
+                    switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
+                }
+
+                Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
+                _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        true,
     )
 }
 
@@ -363,6 +414,7 @@ fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
                 _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        true,
     )
 }
 
@@ -374,7 +426,7 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
         .child("Root password", EditView::new().secret())
         .child("Confirm root password", EditView::new().secret())
         .child(
-            "Administator email",
+            "Administrator email",
             EditView::new().content(&options.email),
         )
         .with_name("password-options");
@@ -396,12 +448,18 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
                     .get_value::<EditView, _>(2)
                     .ok_or("failed to retrieve email")?;
 
+                let email_regex =
+                    Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
+                        .unwrap();
+
                 if root_password.len() < 5 {
-                    Err("password too short")
+                    Err("password too short, must be at least 5 characters long")
                 } else if root_password != confirm_password {
                     Err("passwords do not match")
-                } else if email.ends_with(".invalid") {
+                } else if email == "mail@example.invalid" {
                     Err("invalid email address")
+                } else if !email_regex.is_match(&email) {
+                    Err("Email does not look like a valid address (user@domain.tld)")
                 } else {
                     Ok(PasswordOptions {
                         root_password,
@@ -422,18 +480,29 @@ fn password_dialog(siv: &mut Cursive) -> InstallerView {
                 _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        false,
     )
 }
 
 fn network_dialog(siv: &mut Cursive) -> InstallerView {
     let state = siv.user_data::<InstallerState>().unwrap();
     let options = &state.options.network;
+    let ifaces = state.runtime_info.network.interfaces.values();
+    let ifnames = ifaces
+        .clone()
+        .map(|iface| (iface.render(), iface.name.clone()));
+    let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
+
+    ifaces_selection.sort();
+    ifaces_selection.set_selection(
+        ifnames
+            .clone()
+            .position(|iface| iface.1 == options.ifname)
+            .unwrap_or(ifaces.len() - 1),
+    );
 
     let inner = FormView::new()
-        .child(
-            "Management interface",
-            SelectView::new().popup().with_all_str(vec!["eth0"]),
-        )
+        .child("Management interface", ifaces_selection)
         .child(
             "Hostname (FQDN)",
             EditView::new().content(options.fqdn.to_string()),
@@ -465,7 +534,7 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                     .get_value::<EditView, _>(1)
                     .ok_or("failed to retrieve host FQDN")?
                     .parse::<Fqdn>()
-                    .map_err(|_| "failed to parse hostname".to_owned())?;
+                    .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
 
                 let address = view
                     .get_value::<CidrAddressEditView, _>(2)
@@ -478,7 +547,7 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                     .map_err(|err| err.to_string())?;
 
                 let dns_server = view
-                    .get_value::<EditView, _>(3)
+                    .get_value::<EditView, _>(4)
                     .ok_or("failed to retrieve DNS server address")?
                     .parse::<IpAddr>()
                     .map_err(|err| err.to_string())?;
@@ -487,9 +556,6 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                     Err("host and gateway IP address version must not differ".to_owned())
                 } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
                     Err("host and DNS IP address version must not differ".to_owned())
-                } else if fqdn.to_string().chars().all(|c| c.is_ascii_digit()) {
-                    // Not supported/allowed on Debian
-                    Err("hostname cannot be purely numeric".to_owned())
                 } else if fqdn.to_string().ends_with(".invalid") {
                     Err("hostname does not look valid".to_owned())
                 } else {
@@ -515,6 +581,7 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                 _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        true,
     )
 }
 
@@ -545,7 +612,27 @@ impl TableViewItem for SummaryOption {
 fn summary_dialog(siv: &mut Cursive) -> InstallerView {
     let state = siv.user_data::<InstallerState>().unwrap();
 
-    let inner = LinearLayout::vertical()
+    let mut bbar = LinearLayout::horizontal()
+        .child(abort_install_button())
+        .child(DummyView.full_width())
+        .child(Button::new("Previous", switch_to_prev_screen))
+        .child(DummyView)
+        .child(Button::new("Install", |siv| {
+            let autoreboot = siv
+                .find_name("reboot-after-install")
+                .map(|v: ViewRef<Checkbox>| v.is_checked())
+                .unwrap_or_default();
+
+            siv.with_user_data(|state: &mut InstallerState| {
+                state.options.autoreboot = autoreboot;
+            });
+
+            switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
+        }));
+
+    let _ = bbar.set_focus_index(2); // ignore errors
+
+    let mut inner = LinearLayout::vertical()
         .child(PaddedView::lrtb(
             0,
             0,
@@ -561,35 +648,15 @@ fn summary_dialog(siv: &mut Cursive) -> InstallerView {
         .child(
             LinearLayout::horizontal()
                 .child(DummyView.full_width())
-                .child(Checkbox::new().with_name("reboot-after-install"))
+                .child(Checkbox::new().checked().with_name("reboot-after-install"))
                 .child(
                     TextView::new(" Automatically reboot after successful installation").no_wrap(),
                 )
                 .child(DummyView.full_width()),
         )
-        .child(PaddedView::lrtb(
-            1,
-            1,
-            1,
-            0,
-            LinearLayout::horizontal()
-                .child(abort_install_button())
-                .child(DummyView.full_width())
-                .child(Button::new("Previous", switch_to_prev_screen))
-                .child(DummyView)
-                .child(Button::new("Install", |siv| {
-                    let reboot = siv
-                        .find_name("reboot-after-install")
-                        .map(|v: ViewRef<Checkbox>| v.is_checked())
-                        .unwrap_or_default();
+        .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
 
-                    siv.with_user_data(|state: &mut InstallerState| {
-                        state.options.reboot = reboot;
-                    });
-
-                    switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
-                })),
-        ));
+    let _ = inner.set_focus_index(2); // ignore errors
 
     InstallerView::with_raw(state, inner)
 }
@@ -598,29 +665,6 @@ fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
     // Ensure the screen is updated independently of keyboard events and such
     siv.set_autorefresh(true);
 
-    let state = siv.user_data::<InstallerState>().unwrap();
-    let progress_text = TextContent::new("extracting ..");
-    let progress_bar = ProgressBar::new()
-        .with_task({
-            move |counter| {
-                for _ in 0..100 {
-                    std::thread::sleep(std::time::Duration::from_millis(50));
-                    counter.tick(1);
-                }
-            }
-        })
-        .full_width();
-
-    let inner = PaddedView::lrtb(
-        1,
-        1,
-        1,
-        1,
-        LinearLayout::vertical()
-            .child(PaddedView::lrtb(1, 1, 0, 0, progress_bar))
-            .child(DummyView)
-            .child(TextView::new_with_content(progress_text).center()),
-    );
-
-    InstallerView::with_raw(state, inner)
+    let state = siv.user_data::<InstallerState>().cloned().unwrap();
+    InstallerView::with_raw(&state, InstallProgressView::new(siv))
 }