]> 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 e9ff545d3008b7ee5dec311f63f3f58439e3592e..4c1448248575facea743bd8725e76f69277501da 100644 (file)
@@ -1,52 +1,46 @@
 #![forbid(unsafe_code)]
 
-use std::{
-    collections::HashMap,
-    env,
-    io::{BufRead, BufReader, Write},
-    net::IpAddr,
-    path::PathBuf,
-    str::FromStr,
-    sync::{Arc, Mutex},
-};
+use std::{collections::HashMap, env, net::IpAddr};
 
 use cursive::{
     event::Event,
     theme::{ColorStyle, Effect, PaletteColor, Style},
-    utils::Counter,
     view::{Nameable, Offset, Resizable, ViewWrapper},
     views::{
         Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
-        ProgressBar, ResizedView, ScrollView, SelectView, StackView, TextContent, TextView,
-        ViewRef,
+        ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
     },
     Cursive, CursiveRunnable, ScreenId, View, XY,
 };
 
+use regex::Regex;
+
 mod options;
-use options::*;
+use options::InstallerOptions;
+
+use proxmox_installer_common::{
+    options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
+    setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
+    utils::Fqdn,
+};
 
 mod setup;
-use setup::{InstallConfig, LocaleInfo, RuntimeInfo, SetupInfo};
 
 mod system;
 
-mod utils;
-use utils::Fqdn;
-
 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 PROXMOX_LOGO: &str = r#"
   ____
-   / __ \_________  _  ______ ___  ____  _  __
-  / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/
- / ____/ /  / /_/ />  </ / / / / / /_/ />  <
-/_/   /_/   \____/_/|_/_/ /_/ /_/\____/_/|_|  "#;
+ ____
+|  _ \ _ __ _____  ___ __ ___   _____  __
+| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
+|  __/| | | (_) >  <| | | | | | (_) >  <
+|_|   |_|  \___/_/\_\_| |_| |_|\___/_/\_\ "#;
 
 struct InstallerView {
     view: ResizedView<Dialog>,
@@ -57,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)
     }
@@ -175,9 +169,9 @@ fn main() {
         options: InstallerOptions {
             bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
             timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
-            password: PasswordOptions::defaults_from(&runtime_info),
-            network: NetworkOptions::from(&runtime_info.network),
-            reboot: false,
+            password: Default::default(),
+            network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
+            autoreboot: false,
         },
         setup_info,
         runtime_info,
@@ -190,42 +184,37 @@ fn main() {
     siv.run();
 }
 
-fn installer_setup(in_test_mode: bool) -> Result<(SetupInfo, LocaleInfo, RuntimeInfo), String> {
-    let base_path = if in_test_mode { "./testdir" } else { "/" };
-    let mut path = PathBuf::from(base_path);
-
-    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}"))?
-    };
-
-    let locale_info = {
-        let mut path = path.clone();
-        path.push("locales.json");
-
-        setup::read_json(&path).map_err(|err| format!("Failed to retrieve locale info: {err}"))?
-    };
-
-    let mut runtime_info: RuntimeInfo = {
-        let mut path = path.clone();
-        path.push("run-env-info.json");
+/// 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();
 
-        setup::read_json(&path)
-            .map_err(|err| format!("Failed to retrieve runtime environment info: {err}"))?
-    };
+    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}"));
+            }
+        }
+    }
 
-    system::has_min_requirements(&runtime_info)?;
+    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."
+            ),
+        );
+    }
 
-    runtime_info.disks.sort();
-    if runtime_info.disks.is_empty() {
-        Err("The installer could not find any supported hard disks.".to_owned())
-    } else {
-        Ok((installer_info, locale_info, runtime_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."
+            ),
+        );
     }
 }
 
@@ -240,16 +229,32 @@ 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(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;
     }
 
@@ -266,6 +271,13 @@ fn switch_to_next_screen(
     );
 
     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) {
@@ -273,23 +285,23 @@ fn switch_to_prev_screen(siv: &mut Cursive) {
     siv.set_screen(id);
 }
 
-fn yes_no_dialog(
+fn prompt_dialog(
     siv: &mut Cursive,
     title: &str,
     text: &str,
-    // callback_yes: &'static dyn Fn(&mut Cursive),
-    // callback_no: &'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)
-            .button("No", move |siv| {
+            .button(no_text, move |siv| {
                 siv.pop_layer();
                 callback_no(siv);
             })
-            .button("Yes", move |siv| {
+            .button(yes_text, move |siv| {
                 siv.pop_layer();
                 callback_yes(siv);
             }),
@@ -301,11 +313,13 @@ 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?",
+        "Yes",
         Box::new(Cursive::quit),
+        "No",
         Box::new(|_| {}),
     )
 }
@@ -314,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,
@@ -332,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)
 }
@@ -355,7 +370,7 @@ fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
 
     InstallerView::new(
         &state,
-        BootdiskOptionsView::new(&state.runtime_info.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);
@@ -373,6 +388,7 @@ fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
                 _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        true,
     )
 }
 
@@ -398,6 +414,7 @@ fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
                 _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        true,
     )
 }
 
@@ -409,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");
@@ -431,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 == "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,
@@ -457,20 +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(state.runtime_info.network.interfaces.keys()),
-        )
+        .child("Management interface", ifaces_selection)
         .child(
             "Hostname (FQDN)",
             EditView::new().content(options.fqdn.to_string()),
@@ -502,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)
@@ -515,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())?;
@@ -524,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 {
@@ -552,6 +581,7 @@ fn network_dialog(siv: &mut Cursive) -> InstallerView {
                 _ => siv.add_layer(Dialog::info("Invalid values")),
             }
         }),
+        true,
     )
 }
 
@@ -582,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,
@@ -598,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();
-
-                    siv.with_user_data(|state: &mut InstallerState| {
-                        state.options.reboot = reboot;
-                    });
+        .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
 
-                    switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
-                })),
-        ));
+    let _ = inner.set_focus_index(2); // ignore errors
 
     InstallerView::with_raw(state, inner)
 }
@@ -635,198 +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 cb_sink = siv.cb_sink().clone();
-    let state = siv.user_data::<InstallerState>().unwrap();
-    let progress_text = TextContent::new("starting the installation ..");
-
-    let progress_task = {
-        let progress_text = progress_text.clone();
-        let options = state.options.clone();
-        move |counter: Counter| {
-            let child = {
-                use std::process::{Command, Stdio};
-
-                #[cfg(not(debug_assertions))]
-                let (path, args, envs): (&str, [&str; 1], [(&str, &str); 0]) =
-                    ("proxmox-low-level-installer", ["start-session"], []);
-
-                #[cfg(debug_assertions)]
-                let (path, args, envs) = (
-                    PathBuf::from("../proxmox-low-level-installer")
-                        .canonicalize()
-                        .unwrap(),
-                    ["-t", "start-session"],
-                    [("PERL5LIB", PathBuf::from("..").canonicalize().unwrap())],
-                );
-
-                Command::new(path)
-                    .args(args)
-                    .envs(envs)
-                    .stdin(Stdio::piped())
-                    .stdout(Stdio::piped())
-                    .spawn()
-            };
-
-            let mut child = match child {
-                Ok(child) => child,
-                Err(err) => {
-                    let _ = cb_sink.send(Box::new(move |siv| {
-                        siv.add_layer(
-                            Dialog::text(err.to_string())
-                                .title("Error")
-                                .button("Ok", Cursive::quit),
-                        );
-                    }));
-                    return;
-                }
-            };
-
-            let inner = || {
-                let reader = child.stdout.take().map(BufReader::new)?;
-                let mut writer = child.stdin.take()?;
-
-                serde_json::to_writer(&mut writer, &InstallConfig::from(options)).unwrap();
-                writeln!(writer).unwrap();
-
-                let writer = Arc::new(Mutex::new(writer));
-
-                for line in reader.lines() {
-                    let line = match line {
-                        Ok(line) => line,
-                        Err(_) => break,
-                    };
-
-                    let msg = match line.parse::<UiMessage>() {
-                        Ok(msg) => msg,
-                        Err(_stray) => {
-                            // eprintln!("low-level installer: {stray}");
-                            continue;
-                        }
-                    };
-
-                    match msg {
-                        UiMessage::Info(s) => cb_sink.send(Box::new(|siv| {
-                            siv.add_layer(Dialog::info(s).title("Information"));
-                        })),
-                        UiMessage::Error(s) => cb_sink.send(Box::new(|siv| {
-                            siv.add_layer(Dialog::info(s).title("Error"));
-                        })),
-                        UiMessage::Prompt(s) => cb_sink.send({
-                            let writer = writer.clone();
-                            Box::new(move |siv| {
-                                yes_no_dialog(
-                                    siv,
-                                    "Prompt",
-                                    &s,
-                                    Box::new({
-                                        let writer = writer.clone();
-                                        move |_| {
-                                            if let Ok(mut writer) = writer.lock() {
-                                                let _ = writeln!(writer, "ok");
-                                            }
-                                        }
-                                    }),
-                                    Box::new(move |_| {
-                                        if let Ok(mut writer) = writer.lock() {
-                                            let _ = writeln!(writer);
-                                        }
-                                    }),
-                                );
-                            })
-                        }),
-                        UiMessage::Progress(ratio, s) => {
-                            counter.set(ratio);
-                            progress_text.set_content(s);
-                            Ok(())
-                        }
-                        UiMessage::Finished(success, msg) => {
-                            counter.set(100);
-                            progress_text.set_content(msg.to_owned());
-                            cb_sink.send(Box::new(move |siv| {
-                                let title = if success { "Success" } else { "Failure" };
-                                siv.add_layer(
-                                    Dialog::text(msg)
-                                        .title(title)
-                                        .button("Reboot", |s| s.quit()),
-                                );
-                            }))
-                        }
-                    }
-                    .unwrap();
-                }
-
-                Some(())
-            };
-
-            if inner().is_none() {
-                cb_sink
-                    .send(Box::new(|siv| {
-                        siv.add_layer(
-                            Dialog::text("low-level installer exited early")
-                                .title("Error")
-                                .button("Exit", Cursive::quit),
-                        );
-                    }))
-                    .unwrap();
-            }
-        }
-    };
-
-    let progress_bar = ProgressBar::new().with_task(progress_task).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())
-            .child(PaddedView::lrtb(
-                1,
-                1,
-                1,
-                0,
-                LinearLayout::horizontal().child(abort_install_button()),
-            )),
-    );
-
-    InstallerView::with_raw(state, inner)
-}
-
-enum UiMessage {
-    Info(String),
-    Error(String),
-    Prompt(String),
-    Finished(bool, String),
-    Progress(usize, String),
-}
-
-impl FromStr for UiMessage {
-    type Err = String;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let (ty, rest) = s.split_once(": ").ok_or("invalid message: no type")?;
-
-        match ty {
-            "message" => Ok(UiMessage::Info(rest.to_owned())),
-            "error" => Ok(UiMessage::Error(rest.to_owned())),
-            "prompt" => Ok(UiMessage::Prompt(rest.to_owned())),
-            "finished" => {
-                let (state, rest) = rest.split_once(", ").ok_or("invalid message: no state")?;
-                Ok(UiMessage::Finished(state == "ok", rest.to_owned()))
-            }
-            "progress" => {
-                let (percent, rest) = rest.split_once(' ').ok_or("invalid progress message")?;
-                Ok(UiMessage::Progress(
-                    percent
-                        .parse::<f64>()
-                        .map(|v| (v * 100.).floor() as usize)
-                        .map_err(|err| err.to_string())?,
-                    rest.to_owned(),
-                ))
-            }
-            _ => Err("invalid message type".to_owned()),
-        }
-    }
+    let state = siv.user_data::<InstallerState>().cloned().unwrap();
+    InstallerView::with_raw(&state, InstallProgressView::new(siv))
 }