]> git.proxmox.com Git - pve-installer.git/blame - proxmox-tui-installer/src/main.rs
tui: fix interface sort order
[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
28a55aea 288fn prompt_dialog(
183e2a76
CH
289 siv: &mut Cursive,
290 title: &str,
291 text: &str,
28a55aea 292 yes_text: &str,
deebe07f 293 callback_yes: Box<dyn Fn(&mut Cursive)>,
28a55aea 294 no_text: &str,
deebe07f 295 callback_no: Box<dyn Fn(&mut Cursive)>,
183e2a76
CH
296) {
297 siv.add_layer(
298 Dialog::around(TextView::new(text))
299 .title(title)
28a55aea 300 .button(no_text, move |siv| {
deebe07f
CH
301 siv.pop_layer();
302 callback_no(siv);
303 })
28a55aea 304 .button(yes_text, move |siv| {
deebe07f
CH
305 siv.pop_layer();
306 callback_yes(siv);
307 }),
183e2a76
CH
308 )
309}
310
ccf3b075 311fn trigger_abort_install_dialog(siv: &mut Cursive) {
58869243
CH
312 #[cfg(debug_assertions)]
313 siv.quit();
314
315 #[cfg(not(debug_assertions))]
28a55aea 316 prompt_dialog(
ccf3b075
CH
317 siv,
318 "Abort installation?",
319 "Are you sure you want to abort the installation?",
28a55aea 320 "Yes",
deebe07f 321 Box::new(Cursive::quit),
28a55aea 322 "No",
deebe07f 323 Box::new(|_| {}),
ccf3b075
CH
324 )
325}
326
183e2a76 327fn abort_install_button() -> Button {
ccf3b075 328 Button::new("Abort", trigger_abort_install_dialog)
183e2a76
CH
329}
330
8c9af1e7
CH
331fn get_eula(setup: &SetupInfo) -> String {
332 let mut path = setup.locations.iso.clone();
333 path.push("EULA");
334
335 std::fs::read_to_string(path)
8f5fdd21 336 .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
183e2a76
CH
337}
338
90c0ea37
CH
339fn license_dialog(siv: &mut Cursive) -> InstallerView {
340 let state = siv.user_data::<InstallerState>().unwrap();
341
1d8fcd72
DC
342 let mut bbar = LinearLayout::horizontal()
343 .child(abort_install_button())
344 .child(DummyView.full_width())
345 .child(Button::new("I agree", |siv| {
346 switch_to_next_screen(siv, InstallerStep::Bootdisk, &bootdisk_dialog)
347 }));
348 let _ = bbar.set_focus_index(2); // ignore errors
349
350 let mut inner = LinearLayout::vertical()
183e2a76
CH
351 .child(PaddedView::lrtb(
352 0,
353 0,
354 1,
355 0,
356 TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
357 ))
8b43c2d3 358 .child(Panel::new(ScrollView::new(
8c9af1e7 359 TextView::new(get_eula(&state.setup_info)).center(),
183e2a76 360 )))
1d8fcd72
DC
361 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
362
363 let _ = inner.set_focus_index(2); // ignore errors
183e2a76 364
90c0ea37 365 InstallerView::with_raw(state, inner)
183e2a76
CH
366}
367
e70f1b2f 368fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
a6e00ea6 369 let state = siv.user_data::<InstallerState>().cloned().unwrap();
64220ff1 370
f522afb1 371 InstallerView::new(
90c0ea37 372 &state,
521dfff5 373 BootdiskOptionsView::new(siv, &state.runtime_info, &state.options.bootdisk)
ed191e60 374 .with_name("bootdisk-options"),
f522afb1 375 Box::new(|siv| {
994c4ff0
CH
376 let options = siv.call_on_name("bootdisk-options", BootdiskOptionsView::get_values);
377
378 match options {
379 Some(Ok(options)) => {
380 siv.with_user_data(|state: &mut InstallerState| {
381 state.options.bootdisk = options;
382 });
383
384 switch_to_next_screen(siv, InstallerStep::Timezone, &timezone_dialog);
385 }
386
387 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
388 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 389 }
f522afb1 390 }),
1d8fcd72 391 true,
f522afb1 392 )
64220ff1
CH
393}
394
a142f457 395fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
396 let state = siv.user_data::<InstallerState>().unwrap();
397 let options = &state.options.timezone;
a142f457 398
a142f457 399 InstallerView::new(
90c0ea37 400 state,
e1a11f79 401 TimezoneOptionsView::new(&state.locales, options).with_name("timezone-options"),
a142f457 402 Box::new(|siv| {
e1a11f79 403 let options = siv.call_on_name("timezone-options", TimezoneOptionsView::get_values);
93ebe7bd 404
767843f9
CH
405 match options {
406 Some(Ok(options)) => {
a6e00ea6
CH
407 siv.with_user_data(|state: &mut InstallerState| {
408 state.options.timezone = options;
767843f9
CH
409 });
410
6ab6af4c 411 switch_to_next_screen(siv, InstallerStep::Password, &password_dialog);
767843f9
CH
412 }
413 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
414 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 415 }
a142f457 416 }),
1d8fcd72 417 true,
a142f457 418 )
183e2a76 419}
c2eee468
CH
420
421fn password_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
422 let state = siv.user_data::<InstallerState>().unwrap();
423 let options = &state.options.password;
c2eee468 424
15832d18
CH
425 let inner = FormView::new()
426 .child("Root password", EditView::new().secret())
427 .child("Confirm root password", EditView::new().secret())
90c0ea37 428 .child(
0f39ba93 429 "Administrator email",
90c0ea37
CH
430 EditView::new().content(&options.email),
431 )
ef4957e8 432 .with_name("password-options");
c2eee468 433
ccd34284 434 InstallerView::new(
90c0ea37 435 state,
ccd34284
CH
436 inner,
437 Box::new(|siv| {
15832d18
CH
438 let options = siv.call_on_name("password-options", |view: &mut FormView| {
439 let root_password = view
440 .get_value::<EditView, _>(0)
441 .ok_or("failed to retrieve password")?;
442
443 let confirm_password = view
444 .get_value::<EditView, _>(1)
445 .ok_or("failed to retrieve password confirmation")?;
ef4957e8 446
15832d18
CH
447 let email = view
448 .get_value::<EditView, _>(2)
449 .ok_or("failed to retrieve email")?;
ef4957e8 450
298a4de0
DC
451 let email_regex =
452 Regex::new(r"^[\w\+\-\~]+(\.[\w\+\-\~]+)*@[a-zA-Z0-9\-]+(\.[a-zA-Z0-9\-]+)*$")
453 .unwrap();
454
b82fff5d 455 if root_password.len() < 5 {
fe64752d 456 Err("password too short, must be at least 5 characters long")
b82fff5d 457 } else if root_password != confirm_password {
ef4957e8 458 Err("passwords do not match")
03888174 459 } else if email == "mail@example.invalid" {
b82fff5d 460 Err("invalid email address")
298a4de0
DC
461 } else if !email_regex.is_match(&email) {
462 Err("Email does not look like a valid address (user@domain.tld)")
ef4957e8
CH
463 } else {
464 Ok(PasswordOptions {
465 root_password,
15832d18 466 email,
ef4957e8
CH
467 })
468 }
469 });
470
471 match options {
472 Some(Ok(options)) => {
a6e00ea6
CH
473 siv.with_user_data(|state: &mut InstallerState| {
474 state.options.password = options;
ef4957e8
CH
475 });
476
6ab6af4c 477 switch_to_next_screen(siv, InstallerStep::Network, &network_dialog);
ef4957e8
CH
478 }
479 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
480 _ => siv.add_layer(Dialog::info("Invalid values")),
481 }
ccd34284 482 }),
1d8fcd72 483 false,
ccd34284
CH
484 )
485}
486
487fn network_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37
CH
488 let state = siv.user_data::<InstallerState>().unwrap();
489 let options = &state.options.network;
55e5c9cd
TL
490 let ifaces = state.runtime_info.network.interfaces.values();
491 let ifnames = ifaces
492 .clone()
493 .map(|iface| (iface.render(), iface.name.clone()));
89dfa5c9
TL
494 let mut ifaces_selection = SelectView::new().popup().with_all(ifnames.clone());
495
8f0e9ab0 496 // sort first to always have stable view
89dfa5c9 497 ifaces_selection.sort();
8f0e9ab0
SI
498 let selected = ifaces_selection
499 .iter()
500 .position(|(_label, iface)| *iface == options.ifname)
501 .unwrap_or(ifaces.len() - 1);
502
503 ifaces_selection.set_selection(selected);
ccd34284 504
617f2534 505 let inner = FormView::new()
89dfa5c9 506 .child("Management interface", ifaces_selection)
95c49008
CH
507 .child(
508 "Hostname (FQDN)",
509 EditView::new().content(options.fqdn.to_string()),
510 )
617f2534 511 .child(
ccd34284 512 "IP address (CIDR)",
90c0ea37 513 CidrAddressEditView::new().content(options.address.clone()),
617f2534
CH
514 )
515 .child(
ccd34284
CH
516 "Gateway address",
517 EditView::new().content(options.gateway.to_string()),
617f2534
CH
518 )
519 .child(
1397feec 520 "DNS server address",
ccd34284 521 EditView::new().content(options.dns_server.to_string()),
617f2534 522 )
cd383717 523 .with_name("network-options");
ccd34284 524
947fe360 525 InstallerView::new(
90c0ea37 526 state,
947fe360
CH
527 inner,
528 Box::new(|siv| {
617f2534
CH
529 let options = siv.call_on_name("network-options", |view: &mut FormView| {
530 let ifname = view
531 .get_value::<SelectView, _>(0)
532 .ok_or("failed to retrieve management interface name")?;
cd383717 533
617f2534
CH
534 let fqdn = view
535 .get_value::<EditView, _>(1)
95c49008
CH
536 .ok_or("failed to retrieve host FQDN")?
537 .parse::<Fqdn>()
55dc67ca 538 .map_err(|err| format!("hostname does not look valid:\n\n{err}"))?;
617f2534
CH
539
540 let address = view
541 .get_value::<CidrAddressEditView, _>(2)
542 .ok_or("failed to retrieve host address")?;
543
544 let gateway = view
545 .get_value::<EditView, _>(3)
546 .ok_or("failed to retrieve gateway address")?
547 .parse::<IpAddr>()
548 .map_err(|err| err.to_string())?;
549
550 let dns_server = view
d2b13ba7 551 .get_value::<EditView, _>(4)
617f2534
CH
552 .ok_or("failed to retrieve DNS server address")?
553 .parse::<IpAddr>()
554 .map_err(|err| err.to_string())?;
555
556 if address.addr().is_ipv4() != gateway.is_ipv4() {
557 Err("host and gateway IP address version must not differ".to_owned())
558 } else if address.addr().is_ipv4() != dns_server.is_ipv4() {
559 Err("host and DNS IP address version must not differ".to_owned())
95c49008
CH
560 } else if fqdn.to_string().ends_with(".invalid") {
561 Err("hostname does not look valid".to_owned())
617f2534
CH
562 } else {
563 Ok(NetworkOptions {
564 ifname,
565 fqdn,
566 address,
567 gateway,
568 dns_server,
569 })
570 }
cd383717
CH
571 });
572
617f2534
CH
573 match options {
574 Some(Ok(options)) => {
a6e00ea6
CH
575 siv.with_user_data(|state: &mut InstallerState| {
576 state.options.network = options;
617f2534 577 });
cd383717 578
6ab6af4c 579 switch_to_next_screen(siv, InstallerStep::Summary, &summary_dialog);
617f2534
CH
580 }
581 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
582 _ => siv.add_layer(Dialog::info("Invalid values")),
cd383717 583 }
947fe360 584 }),
1d8fcd72 585 true,
947fe360
CH
586 )
587}
588
b7828c87 589pub struct SummaryOption {
947fe360
CH
590 name: &'static str,
591 value: String,
592}
593
594impl SummaryOption {
595 pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self {
596 Self {
597 name,
598 value: value.into(),
599 }
600 }
601}
602
603impl TableViewItem for SummaryOption {
604 fn get_column(&self, name: &str) -> String {
605 match name {
606 "name" => self.name.to_owned(),
607 "value" => self.value.clone(),
608 _ => unreachable!(),
609 }
610 }
611}
612
613fn summary_dialog(siv: &mut Cursive) -> InstallerView {
90c0ea37 614 let state = siv.user_data::<InstallerState>().unwrap();
947fe360 615
1d8fcd72
DC
616 let mut bbar = LinearLayout::horizontal()
617 .child(abort_install_button())
618 .child(DummyView.full_width())
619 .child(Button::new("Previous", switch_to_prev_screen))
620 .child(DummyView)
621 .child(Button::new("Install", |siv| {
622 let autoreboot = siv
623 .find_name("reboot-after-install")
624 .map(|v: ViewRef<Checkbox>| v.is_checked())
625 .unwrap_or_default();
626
627 siv.with_user_data(|state: &mut InstallerState| {
628 state.options.autoreboot = autoreboot;
629 });
630
631 switch_to_next_screen(siv, InstallerStep::Install, &install_progress_dialog);
632 }));
633
fcb51d73 634 let _ = bbar.set_focus_index(2); // ignore errors
1d8fcd72
DC
635
636 let mut inner = LinearLayout::vertical()
947fe360
CH
637 .child(PaddedView::lrtb(
638 0,
639 0,
640 1,
641 2,
642 TableView::new()
643 .columns(&[
644 ("name".to_owned(), "Option".to_owned()),
645 ("value".to_owned(), "Selected value".to_owned()),
646 ])
b107a892 647 .items(state.options.to_summary(&state.locales)),
947fe360
CH
648 ))
649 .child(
650 LinearLayout::horizontal()
651 .child(DummyView.full_width())
a84f277b 652 .child(Checkbox::new().checked().with_name("reboot-after-install"))
947fe360
CH
653 .child(
654 TextView::new(" Automatically reboot after successful installation").no_wrap(),
655 )
656 .child(DummyView.full_width()),
657 )
1d8fcd72 658 .child(PaddedView::lrtb(1, 1, 1, 0, bbar));
ea15ca43 659
1d8fcd72 660 let _ = inner.set_focus_index(2); // ignore errors
947fe360 661
90c0ea37 662 InstallerView::with_raw(state, inner)
c2eee468 663}
81f9348c
CH
664
665fn install_progress_dialog(siv: &mut Cursive) -> InstallerView {
666 // Ensure the screen is updated independently of keyboard events and such
667 siv.set_autorefresh(true);
668
dba905bf
CH
669 let state = siv.user_data::<InstallerState>().cloned().unwrap();
670 InstallerView::with_raw(&state, InstallProgressView::new(siv))
3285f0ad 671}