]> git.proxmox.com Git - pve-installer.git/blame - proxmox-tui-installer/src/main.rs
tui: move install progress dialog into own view module
[pve-installer.git] / proxmox-tui-installer / src / main.rs
CommitLineData
2789d95b
CH
1#![forbid(unsafe_code)]
2
dba905bf 3use std::{collections::HashMap, env, net::IpAddr};
83071f35 4
183e2a76 5use cursive::{
ccf3b075 6 event::Event,
08ad8ed6
CH
7 theme::{ColorStyle, Effect, PaletteColor, Style},
8 view::{Nameable, Offset, Resizable, ViewWrapper},
183e2a76 9 views::{
08ad8ed6 10 Button, Checkbox, Dialog, DummyView, EditView, Layer, LinearLayout, PaddedView, Panel,
dba905bf 11 ResizedView, ScrollView, SelectView, StackView, TextView, ViewRef,
183e2a76 12 },
08ad8ed6 13 Cursive, CursiveRunnable, ScreenId, View, XY,
183e2a76 14};
32368ac3 15
298a4de0
DC
16use regex::Regex;
17
32368ac3 18mod options;
86c48f76
AL
19use options::InstallerOptions;
20
21use proxmox_installer_common::{
22 options::{BootdiskOptions, NetworkOptions, PasswordOptions, TimezoneOptions},
9a848580 23 setup::{installer_setup, LocaleInfo, ProxmoxProduct, RuntimeInfo, SetupInfo},
86c48f76
AL
24 utils::Fqdn,
25};
32368ac3
WB
26
27mod setup;
32368ac3
WB
28
29mod system;
30
32368ac3 31mod views;
e1a11f79 32use views::{
dba905bf
CH
33 BootdiskOptionsView, CidrAddressEditView, FormView, InstallProgressView, TableView,
34 TableViewItem, TimezoneOptionsView,
e1a11f79 35};
183e2a76
CH
36
37// TextView::center() seems to garble the first two lines, so fix it manually here.
08ad8ed6 38const PROXMOX_LOGO: &str = r#"
e730b3fe
CH
39 ____
40| _ \ _ __ _____ ___ __ ___ _____ __
41| |_) | '__/ _ \ \/ / '_ ` _ \ / _ \ \/ /
42| __/| | | (_) > <| | | | | | (_) > <
43|_| |_| \___/_/\_\_| |_| |_|\___/_/\_\ "#;
183e2a76
CH
44
45struct InstallerView {
08ad8ed6 46 view: ResizedView<Dialog>,
183e2a76
CH
47}
48
49impl InstallerView {
90c0ea37
CH
50 pub fn new<T: View>(
51 state: &InstallerState,
52 view: T,
53 next_cb: Box<dyn Fn(&mut Cursive)>,
1d8fcd72 54 focus_next: bool,
90c0ea37 55 ) -> Self {
1d8fcd72
DC
56 let mut bbar = LinearLayout::horizontal()
57 .child(abort_install_button())
58 .child(DummyView.full_width())
59 .child(Button::new("Previous", switch_to_prev_screen))
60 .child(DummyView)
61 .child(Button::new("Next", next_cb));
62 let _ = bbar.set_focus_index(4); // ignore errors
63 let mut inner = LinearLayout::vertical()
8807d547 64 .child(PaddedView::lrtb(0, 0, 1, 1, view))
1d8fcd72
DC
65 .child(PaddedView::lrtb(1, 1, 0, 0, bbar));
66 if focus_next {
67 let _ = inner.set_focus_index(1); // ignore errors
68 }
f522afb1 69
90c0ea37 70 Self::with_raw(state, inner)
f522afb1
CH
71 }
72
90c0ea37
CH
73 pub fn with_raw(state: &InstallerState, view: impl View) -> Self {
74 let setup = &state.setup_info;
75
90c0ea37
CH
76 let title = format!(
77 "{} ({}-{}) Installer",
fbfd1838 78 setup.config.fullname, setup.iso_info.release, setup.iso_info.isorelease
90c0ea37
CH
79 );
80
08ad8ed6 81 let inner = Dialog::around(view).title(title);
8b43c2d3 82
183e2a76 83 Self {
8b43c2d3
CH
84 // Limit the maximum to something reasonable, such that it won't get spread out much
85 // depending on the screen.
86 view: ResizedView::with_max_size((120, 40), inner),
183e2a76
CH
87 }
88 }
89}
90
91impl ViewWrapper for InstallerView {
08ad8ed6
CH
92 cursive::wrap_impl!(self.view: ResizedView<Dialog>);
93}
94
95struct InstallerBackgroundView {
96 view: StackView,
97}
98
99impl InstallerBackgroundView {
1936156e 100 pub fn new() -> Self {
08ad8ed6
CH
101 let style = Style {
102 effects: Effect::Bold.into(),
103 color: ColorStyle::back(PaletteColor::View),
104 };
105
106 let mut view = StackView::new();
107 view.add_fullscreen_layer(Layer::with_color(
108 DummyView
109 .full_width()
1936156e 110 .fixed_height(PROXMOX_LOGO.lines().count() + 1),
08ad8ed6
CH
111 ColorStyle::back(PaletteColor::View),
112 ));
113 view.add_transparent_layer_at(
114 XY {
115 x: Offset::Center,
116 y: Offset::Absolute(0),
117 },
1936156e 118 TextView::new(PROXMOX_LOGO).style(style),
08ad8ed6
CH
119 );
120
121 Self { view }
122 }
123}
124
125impl ViewWrapper for InstallerBackgroundView {
126 cursive::wrap_impl!(self.view: StackView);
183e2a76
CH
127}
128
6ab6af4c
CH
129#[derive(Clone, Eq, Hash, PartialEq)]
130enum InstallerStep {
131 Licence,
132 Bootdisk,
133 Timezone,
134 Password,
135 Network,
136 Summary,
81f9348c 137 Install,
6ab6af4c
CH
138}
139
ed191e60 140#[derive(Clone)]
a6e00ea6 141struct InstallerState {
ed191e60 142 options: InstallerOptions,
4296d200 143 setup_info: SetupInfo,
2473d5dc 144 runtime_info: RuntimeInfo,
4296d200 145 locales: LocaleInfo,
6ab6af4c 146 steps: HashMap<InstallerStep, ScreenId>,
cae15523 147 in_test_mode: bool,
ed191e60
CH
148}
149
183e2a76
CH
150fn main() {
151 let mut siv = cursive::termion();
152
cae15523
CH
153 let in_test_mode = match env::args().nth(1).as_deref() {
154 Some("-t") => true,
155
156 // Always force the test directory in debug builds
c0706374 157 _ => cfg!(debug_assertions),
cae15523
CH
158 };
159
2f0b4f7c 160 let (setup_info, locales, runtime_info) = match installer_setup(in_test_mode) {
4296d200
CH
161 Ok(result) => result,
162 Err(err) => initial_setup_error(&mut siv, &err),
163 };
130fb96a 164
ccf3b075
CH
165 siv.clear_global_callbacks(Event::CtrlChar('c'));
166 siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog);
167
a6e00ea6 168 siv.set_user_data(InstallerState {
ed191e60 169 options: InstallerOptions {
5dd6b6ed 170 bootdisk: BootdiskOptions::defaults_from(&runtime_info.disks[0]),
0e42d278 171 timezone: TimezoneOptions::defaults_from(&runtime_info, &locales),
d09fe568 172 password: Default::default(),
2f0b4f7c 173 network: NetworkOptions::defaults_from(&setup_info, &runtime_info.network),
f81ba3ed 174 autoreboot: false,
e9557e62 175 },
2f0b4f7c 176 setup_info,
2473d5dc 177 runtime_info,
4296d200 178 locales,
6ab6af4c 179 steps: HashMap::new(),
cae15523 180 in_test_mode,
e9557e62
CH
181 });
182
6ab6af4c 183 switch_to_next_screen(&mut siv, InstallerStep::Licence, &license_dialog);
183e2a76
CH
184 siv.run();
185}
186
79ae84de
CH
187/// Anything that can be done late in the setup and will not result in fatal errors.
188fn installer_setup_late(siv: &mut Cursive) {
636da45e 189 let state = siv.user_data::<InstallerState>().cloned().unwrap();
79ae84de
CH
190
191 if !state.in_test_mode {
192 let kmap_id = &state.options.timezone.kb_layout;
193 if let Some(kmap) = state.locales.kmap.get(kmap_id) {
194 if let Err(err) = system::set_keyboard_layout(kmap) {
082f3a70 195 display_setup_warning(siv, &format!("Failed to apply keyboard layout: {err}"));
79ae84de
CH
196 }
197 }
198 }
082f3a70 199
f9960aa7
NU
200 if state.runtime_info.total_memory < 1024 {
201 display_setup_warning(
202 siv,
203 concat!(
204 "Less than 1 GiB of usable memory detected, installation will probably fail.\n\n",
205 "See 'System Requirements' in the documentation."
206 ),
207 );
208 }
209
636da45e
CH
210 if state.setup_info.config.product == ProxmoxProduct::PVE && !state.runtime_info.hvm_supported {
211 display_setup_warning(
212 siv,
213 concat!(
214 "No support for hardware-accelerated KVM virtualization detected.\n\n",
215 "Check BIOS settings for Intel VT / AMD-V / SVM."
216 ),
217 );
082f3a70 218 }
79ae84de
CH
219}
220
4296d200
CH
221fn initial_setup_error(siv: &mut CursiveRunnable, message: &str) -> ! {
222 siv.add_layer(
223 Dialog::around(TextView::new(message))
224 .title("Installer setup error")
225 .button("Ok", Cursive::quit),
226 );
227 siv.run();
228
229 std::process::exit(1);
230}
231
082f3a70
CH
232fn display_setup_warning(siv: &mut Cursive, message: &str) {
233 siv.add_layer(Dialog::info(message).title("Warning"));
234}
235
6ab6af4c
CH
236fn switch_to_next_screen(
237 siv: &mut Cursive,
238 step: InstallerStep,
239 constructor: &dyn Fn(&mut Cursive) -> InstallerView,
240) {
08ad8ed6 241 let state = siv.user_data::<InstallerState>().cloned().unwrap();
79ae84de 242 let is_first_screen = state.steps.is_empty();
08ad8ed6 243
6ab6af4c 244 // Check if the screen already exists; if yes, then simply switch to it.
08ad8ed6
CH
245 if let Some(screen_id) = state.steps.get(&step) {
246 siv.set_screen(*screen_id);
93f4fdfa
CH
247
248 // The summary view cannot be cached (otherwise it would display stale values). Thus
249 // replace it if the screen is switched to.
250 // TODO: Could be done by e.g. having all the main dialog views implement some sort of
251 // .refresh(), which can be called if the view is switched to.
252 if step == InstallerStep::Summary {
253 let view = constructor(siv);
254 siv.screen_mut().pop_layer();
255 siv.screen_mut().add_layer(view);
256 }
257
08ad8ed6 258 return;
6ab6af4c
CH
259 }
260
5e73fcfe 261 let v = constructor(siv);
6ab6af4c
CH
262 let screen = siv.add_active_screen();
263 siv.with_user_data(|state: &mut InstallerState| state.steps.insert(step, screen));
08ad8ed6 264
1936156e
CH
265 siv.screen_mut().add_transparent_layer_at(
266 XY {
267 x: Offset::Parent(0),
268 y: Offset::Parent(0),
269 },
270 InstallerBackgroundView::new(),
271 );
08ad8ed6 272
5e73fcfe 273 siv.screen_mut().add_layer(v);
79ae84de
CH
274
275 // If this is the first screen to be added, execute our late setup first.
276 // Needs to be done here, at the end, to ensure that any potential layers get added to
277 // the right screen and are on top.
278 if is_first_screen {
279 installer_setup_late(siv);
280 }
183e2a76
CH
281}
282
64220ff1
CH
283fn switch_to_prev_screen(siv: &mut Cursive) {
284 let id = siv.active_screen().saturating_sub(1);
285 siv.set_screen(id);
286}
287
183e2a76
CH
288fn yes_no_dialog(
289 siv: &mut Cursive,
290 title: &str,
291 text: &str,
deebe07f
CH
292 callback_yes: Box<dyn Fn(&mut Cursive)>,
293 callback_no: Box<dyn Fn(&mut Cursive)>,
183e2a76
CH
294) {
295 siv.add_layer(
296 Dialog::around(TextView::new(text))
297 .title(title)
deebe07f
CH
298 .button("No", move |siv| {
299 siv.pop_layer();
300 callback_no(siv);
301 })
302 .button("Yes", move |siv| {
303 siv.pop_layer();
304 callback_yes(siv);
305 }),
183e2a76
CH
306 )
307}
308
ccf3b075 309fn trigger_abort_install_dialog(siv: &mut Cursive) {
58869243
CH
310 #[cfg(debug_assertions)]
311 siv.quit();
312
313 #[cfg(not(debug_assertions))]
ccf3b075
CH
314 yes_no_dialog(
315 siv,
316 "Abort installation?",
317 "Are you sure you want to abort the installation?",
deebe07f
CH
318 Box::new(Cursive::quit),
319 Box::new(|_| {}),
ccf3b075
CH
320 )
321}
322
183e2a76 323fn abort_install_button() -> Button {
ccf3b075 324 Button::new("Abort", trigger_abort_install_dialog)
183e2a76
CH
325}
326
8c9af1e7
CH
327fn get_eula(setup: &SetupInfo) -> String {
328 let mut path = setup.locations.iso.clone();
329 path.push("EULA");
330
331 std::fs::read_to_string(path)
8f5fdd21 332 .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
183e2a76
CH
333}
334
90c0ea37
CH
335fn license_dialog(siv: &mut Cursive) -> InstallerView {
336 let state = siv.user_data::<InstallerState>().unwrap();
337
1d8fcd72
DC
338 let mut bbar = LinearLayout::horizontal()
339 .child(abort_install_button())
340 .child(DummyView.full_width())
341 .child(Button::new("I agree", |siv| {
342 switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
343 }));
344 let _ = bbar.set_focus_index(2); // ignore errors
345
346 let mut inner = LinearLayout::vertical()
183e2a76
CH
347 .child(PaddedView::lrtb(
348 0,
349 0,
350 1,
351 0,
352 TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
353 ))
8b43c2d3 354 .child(Panel::new(ScrollView::new(
8c9af1e7 355 TextView::new(get_eula(&state.setup_info)).center(),
183e2a76 356 )))
1d8fcd72
DC
357 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
358
359 let _ = inner.set_focus_index(2); // ignore errors
183e2a76 360
90c0ea37 361 InstallerView::with_raw(state, inner)
183e2a76
CH
362}
363
e70f1b2f 364fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
a6e00ea6 365 let state = siv.user_data::<InstallerState>().cloned().unwrap();
64220ff1 366
f522afb1 367 InstallerView::new(
90c0ea37 368 &state,
521dfff5 369 BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk)
ed191e60 370 .with_name("bootdisk-options"),
f522afb1 371 Box::new(|siv| {
994c4ff0
CH
372 let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
373
374 match options {
375 Some(Ok(options)) => {
376 siv.with_user_data(|state: &mut InstallerState| {
377 state.options.bootdisk = options;
378 });
379
380 switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
381 }
382
383 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
384 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 385 }
f522afb1 386 }),
1d8fcd72 387 true,
f522afb1 388 )
64220ff1
CH
389}
390
a142f457 391fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
392 let state = siv.user_data::<InstallerState>().unwrap();
393 let options = &state.options.timezone;
a142f457 394
a142f457 395 InstallerView::new(
90c0ea37 396 state,
e1a11f79 397 TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"),
a142f457 398 Box::new(|siv| {
e1a11f79 399 let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values);
93ebe7bd 400
767843f9
CH
401 match options {
402 Some(Ok(options)) => {
a6e00ea6
CH
403 siv.with_user_data(|state: &mut InstallerState| {
404 state.options.timezone = options;
767843f9
CH
405 });
406
6ab6af4c 407 switch_to_next_screen(siv, InstallerStep::Password, &password_dialog);
767843f9
CH
408 }
409 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
410 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 411 }
a142f457 412 }),
1d8fcd72 413 true,
a142f457 414 )
183e2a76 415}
c2eee468
CH
416
417fn password_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
418 let state = siv.user_data::<InstallerState>().unwrap();
419 let options = &state.options.password;
c2eee468 420
15832d18
CH
421 let inner = FormView::new()
422 .child("Root password", EditView::new().secret())
423 .child("Confirm root password", EditView::new().secret())
90c0ea37 424 .child(
0f39ba93 425 "Administrator email",
90c0ea37
CH
426 EditView::new().content(&options.email),
427 )
ef4957e8 428 .with_name("password-options");
c2eee468 429
ccd34284 430 InstallerView::new(
90c0ea37 431 state,
ccd34284
CH
432 inner,
433 Box::new(|siv| {
15832d18
CH
434 let options = siv.call_on_name("password-options", |view: &mut FormView| {
435 let root_password = view
436 .get_value::<EditView, _>(0)
437 .ok_or("failed to retrieve password")?;
438
439 let confirm_password = view
440 .get_value::<EditView, _>(1)
441 .ok_or("failed to retrieve password confirmation")?;
ef4957e8 442
15832d18
CH
443 let email = view
444 .get_value::<EditView, _>(2)
445 .ok_or("failed to retrieve email")?;
ef4957e8 446
298a4de0
DC
447 let email_regex =
448 Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
449 .unwrap();
450
b82fff5d 451 if root_password.len() < 5 {
fe64752d 452 Err("password too short, must be at least 5 characters long")
b82fff5d 453 } else if root_password != confirm_password {
ef4957e8 454 Err("passwords do not match")
03888174 455 } else if email == "mail@example.invalid" {
b82fff5d 456 Err("invalid email address")
298a4de0
DC
457 } else if !email_regex.is_match(&email) {
458 Err("Email does not look like a valid address (user@domain.tld)")
ef4957e8
CH
459 } else {
460 Ok(PasswordOptions {
461 root_password,
15832d18 462 email,
ef4957e8
CH
463 })
464 }
465 });
466
467 match options {
468 Some(Ok(options)) => {
a6e00ea6
CH
469 siv.with_user_data(|state: &mut InstallerState| {
470 state.options.password = options;
ef4957e8
CH
471 });
472
6ab6af4c 473 switch_to_next_screen(siv, InstallerStep::Network, &network_dialog);
ef4957e8
CH
474 }
475 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
476 _ => siv.add_layer(Dialog::info("Invalid values")),
477 }
ccd34284 478 }),
1d8fcd72 479 false,
ccd34284
CH
480 )
481}
482
483fn network_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
484 let state = siv.user_data::<InstallerState>().unwrap();
485 let options = &state.options.network;
55e5c9cd
TL
486 let ifaces = state.runtime_info.network.interfaces.values();
487 let ifnames = ifaces
488 .clone()
489 .map(|iface| (iface.render(), iface.name.clone()));
89dfa5c9
TL
490 let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
491
492 ifaces_selection.sort();
493 ifaces_selection.set_selection(
494 ifnames
495 .clone()
57161139 496 .position(|iface| iface.1 == options.ifname)
89dfa5c9
TL
497 .unwrap_or(ifaces.len() - 1),
498 );
ccd34284 499
617f2534 500 let inner = FormView::new()
89dfa5c9 501 .child("Management interface", ifaces_selection)
95c49008
CH
502 .child(
503 "Hostname (FQDN)",
504 EditView::new().content(options.fqdn.to_string()),
505 )
617f2534 506 .child(
ccd34284 507 "IP address (CIDR)",
90c0ea37 508 CidrAddressEditView::new().content(options.address.clone()),
617f2534
CH
509 )
510 .child(
ccd34284
CH
511 "Gateway address",
512 EditView::new().content(options.gateway.to_string()),
617f2534
CH
513 )
514 .child(
1397feec 515 "DNS server address",
ccd34284 516 EditView::new().content(options.dns_server.to_string()),
617f2534 517 )
cd383717 518 .with_name("network-options");
ccd34284 519
947fe360 520 InstallerView::new(
90c0ea37 521 state,
947fe360
CH
522 inner,
523 Box::new(|siv| {
617f2534
CH
524 let options = siv.call_on_name("network-options", |view: &mut FormView| {
525 let ifname = view
526 .get_value::<SelectView, _>(0)
527 .ok_or("failed to retrieve management interface name")?;
cd383717 528
617f2534
CH
529 let fqdn = view
530 .get_value::<EditView, _>(1)
95c49008
CH
531 .ok_or("failed to retrieve host FQDN")?
532 .parse::<Fqdn>()
55dc67ca 533 .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
617f2534
CH
534
535 let address = view
536 .get_value::<CidrAddressEditView, _>(2)
537 .ok_or("failed to retrieve host address")?;
538
539 let gateway = view
540 .get_value::<EditView, _>(3)
541 .ok_or("failed to retrieve gateway address")?
542 .parse::<IpAddr>()
543 .map_err(|err| err.to_string())?;
544
545 let dns_server = view
d2b13ba7 546 .get_value::<EditView, _>(4)
617f2534
CH
547 .ok_or("failed to retrieve DNS server address")?
548 .parse::<IpAddr>()
549 .map_err(|err| err.to_string())?;
550
551 if address.addr().is_ipv4() != gateway.is_ipv4() {
552 Err("host and gateway IP address version must not differ".to_owned())
553 } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
554 Err("host and DNS IP address version must not differ".to_owned())
95c49008
CH
555 } else if fqdn.to_string().ends_with(".invalid") {
556 Err("hostname does not look valid".to_owned())
617f2534
CH
557 } else {
558 Ok(NetworkOptions {
559 ifname,
560 fqdn,
561 address,
562 gateway,
563 dns_server,
564 })
565 }
cd383717
CH
566 });
567
617f2534
CH
568 match options {
569 Some(Ok(options)) => {
a6e00ea6
CH
570 siv.with_user_data(|state: &mut InstallerState| {
571 state.options.network = options;
617f2534 572 });
cd383717 573
6ab6af4c 574 switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog);
617f2534
CH
575 }
576 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
577 _ => siv.add_layer(Dialog::info("Invalid values")),
cd383717 578 }
947fe360 579 }),
1d8fcd72 580 true,
947fe360
CH
581 )
582}
583
b7828c87 584pub struct SummaryOption {
947fe360
CH
585 name: &'static str,
586 value: String,
587}
588
589impl SummaryOption {
590 pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self {
591 Self {
592 name,
593 value: value.into(),
594 }
595 }
596}
597
598impl TableViewItem for SummaryOption {
599 fn get_column(&self, name: &str) -> String {
600 match name {
601 "name" => self.name.to_owned(),
602 "value" => self.value.clone(),
603 _ => unreachable!(),
604 }
605 }
606}
607
608fn summary_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37 609 let state = siv.user_data::<InstallerState>().unwrap();
947fe360 610
1d8fcd72
DC
611 let mut bbar = LinearLayout::horizontal()
612 .child(abort_install_button())
613 .child(DummyView.full_width())
614 .child(Button::new("Previous", switch_to_prev_screen))
615 .child(DummyView)
616 .child(Button::new("Install", |siv| {
617 let autoreboot = siv
618 .find_name("reboot-after-install")
619 .map(|v: ViewRef<Checkbox>| v.is_checked())
620 .unwrap_or_default();
621
622 siv.with_user_data(|state: &mut InstallerState| {
623 state.options.autoreboot = autoreboot;
624 });
625
626 switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
627 }));
628
fcb51d73 629 let _ = bbar.set_focus_index(2); // ignore errors
1d8fcd72
DC
630
631 let mut inner = LinearLayout::vertical()
947fe360
CH
632 .child(PaddedView::lrtb(
633 0,
634 0,
635 1,
636 2,
637 TableView::new()
638 .columns(&[
639 ("name".to_owned(), "Option".to_owned()),
640 ("value".to_owned(), "Selected value".to_owned()),
641 ])
b107a892 642 .items(state.options.to_summary(&state.locales)),
947fe360
CH
643 ))
644 .child(
645 LinearLayout::horizontal()
646 .child(DummyView.full_width())
a84f277b 647 .child(Checkbox::new().checked().with_name("reboot-after-install"))
947fe360
CH
648 .child(
649 TextView::new(" Automatically reboot after successful installation").no_wrap(),
650 )
651 .child(DummyView.full_width()),
652 )
1d8fcd72 653 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
ea15ca43 654
1d8fcd72 655 let _ = inner.set_focus_index(2); // ignore errors
947fe360 656
90c0ea37 657 InstallerView::with_raw(state, inner)
c2eee468 658}
81f9348c
CH
659
660fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
661 // Ensure the screen is updated independently of keyboard events and such
662 siv.set_autorefresh(true);
663
dba905bf
CH
664 let state = siv.user_data::<InstallerState>().cloned().unwrap();
665 InstallerView::with_raw(&state, InstallProgressView::new(siv))
3285f0ad 666}