]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/bootdisk.rs
html: pbs: fix missing <br> in template after feature list
[pve-installer.git] / proxmox-tui-installer / src / views / bootdisk.rs
1 use std::{cell::RefCell, marker::PhantomData, rc::Rc};
2
3 use cursive::{
4 view::{Nameable, Resizable, ViewWrapper},
5 views::{
6 Button, Dialog, DummyView, LinearLayout, NamedView, PaddedView, Panel, ScrollView,
7 SelectView, TextView,
8 },
9 Cursive, View,
10 };
11
12 use super::{DiskSizeEditView, FormView, IntegerEditView};
13 use crate::options::FS_TYPES;
14 use crate::InstallerState;
15
16 use proxmox_installer_common::{
17 disk_checks::{
18 check_btrfs_raid_config, check_disks_4kn_legacy_boot, check_for_duplicate_disks,
19 check_zfs_raid_config,
20 },
21 options::{
22 AdvancedBootdiskOptions, BootdiskOptions, BtrfsBootdiskOptions, Disk, FsType,
23 LvmBootdiskOptions, ZfsBootdiskOptions, ZFS_CHECKSUM_OPTIONS, ZFS_COMPRESS_OPTIONS,
24 },
25 setup::{BootType, ProductConfig, ProxmoxProduct, RuntimeInfo},
26 };
27
28 /// OpenZFS specifies 64 MiB as the absolute minimum:
29 /// https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max
30 const ZFS_ARC_MIN_SIZE_MIB: usize = 64; // MiB
31
32 /// Convience wrapper when needing to take a (interior-mutable) reference to `BootdiskOptions`.
33 /// Interior mutability is safe for this case, as it is completely single-threaded.
34 pub type BootdiskOptionsRef = Rc<RefCell<BootdiskOptions>>;
35
36 pub struct BootdiskOptionsView {
37 view: LinearLayout,
38 advanced_options: BootdiskOptionsRef,
39 boot_type: BootType,
40 }
41
42 impl BootdiskOptionsView {
43 pub fn new(siv: &mut Cursive, runinfo: &RuntimeInfo, options: &BootdiskOptions) -> Self {
44 let advanced_options = Rc::new(RefCell::new(options.clone()));
45
46 let bootdisk_form = FormView::new()
47 .child(
48 "Target harddisk",
49 target_bootdisk_selectview(
50 &runinfo.disks,
51 advanced_options.clone(),
52 // At least one disk must always exist to even get to this point,
53 // see proxmox_installer_common::setup::installer_setup()
54 &options.disks[0],
55 ),
56 )
57 .with_name("bootdisk-options-target-disk");
58
59 let product_conf = siv
60 .user_data::<InstallerState>()
61 .map(|state| state.setup_info.config.clone())
62 .unwrap(); // Safety: InstallerState must always be set
63
64 let advanced_button = LinearLayout::horizontal()
65 .child(DummyView.full_width())
66 .child(Button::new("Advanced options", {
67 let runinfo = runinfo.clone();
68 let options = advanced_options.clone();
69 move |siv| {
70 siv.add_layer(advanced_options_view(
71 &runinfo,
72 options.clone(),
73 product_conf.clone(),
74 ));
75 }
76 }));
77
78 let view = LinearLayout::vertical()
79 .child(bootdisk_form)
80 .child(DummyView)
81 .child(advanced_button);
82
83 let boot_type = siv
84 .user_data::<InstallerState>()
85 .map(|state| state.runtime_info.boot_type)
86 .unwrap_or(BootType::Bios);
87
88 Self {
89 view,
90 advanced_options,
91 boot_type,
92 }
93 }
94
95 pub fn get_values(&mut self) -> Result<BootdiskOptions, String> {
96 // The simple disk selector, as well as the advanced bootdisk dialog save their
97 // info on submit directly to the shared `BootdiskOptionsRef` - so just clone() + return
98 // it.
99 let options = (*self.advanced_options).clone().into_inner();
100 check_disks_4kn_legacy_boot(self.boot_type, &options.disks)?;
101 Ok(options)
102 }
103 }
104
105 impl ViewWrapper for BootdiskOptionsView {
106 cursive::wrap_impl!(self.view: LinearLayout);
107 }
108
109 struct AdvancedBootdiskOptionsView {
110 view: LinearLayout,
111 }
112
113 impl AdvancedBootdiskOptionsView {
114 fn new(
115 runinfo: &RuntimeInfo,
116 options_ref: BootdiskOptionsRef,
117 product_conf: ProductConfig,
118 ) -> Self {
119 let filter_btrfs =
120 |fstype: &&FsType| -> bool { product_conf.enable_btrfs || !fstype.is_btrfs() };
121 let options = (*options_ref).borrow();
122
123 let fstype_select = SelectView::new()
124 .popup()
125 .with_all(
126 FS_TYPES
127 .iter()
128 .filter(filter_btrfs)
129 .map(|t| (t.to_string(), *t)),
130 )
131 .selected(
132 FS_TYPES
133 .iter()
134 .filter(filter_btrfs)
135 .position(|t| *t == options.fstype)
136 .unwrap_or_default(),
137 )
138 .on_submit({
139 let options_ref = options_ref.clone();
140 move |siv, fstype| {
141 Self::fstype_on_submit(siv, fstype, options_ref.clone());
142 }
143 });
144
145 let mut view = LinearLayout::vertical()
146 .child(DummyView.full_width())
147 .child(FormView::new().child("Filesystem", fstype_select))
148 .child(DummyView.full_width());
149
150 // Create the appropriate (inner) advanced options view
151 match &options.advanced {
152 AdvancedBootdiskOptions::Lvm(lvm) => view.add_child(LvmBootdiskOptionsView::new(
153 &options.disks[0],
154 lvm,
155 &product_conf,
156 )),
157 AdvancedBootdiskOptions::Zfs(zfs) => {
158 view.add_child(ZfsBootdiskOptionsView::new(runinfo, zfs, &product_conf))
159 }
160 AdvancedBootdiskOptions::Btrfs(btrfs) => {
161 view.add_child(BtrfsBootdiskOptionsView::new(&runinfo.disks, btrfs))
162 }
163 };
164
165 Self { view }
166 }
167
168 /// Called when a new filesystem type is choosen by the user.
169 /// It first creates the inner (filesystem-specific) options view according to the selected
170 /// filesytem type.
171 /// Further, it replaces the (outer) bootdisk selector in the main dialog, either with a
172 /// selector for LVM configurations or a simple label displaying the chosen RAID for ZFS and
173 /// Btrfs configurations.
174 ///
175 /// # Arguments
176 /// * `siv` - Cursive instance
177 /// * `fstype` - The chosen filesystem type by the user, for which the UI should be
178 /// updated accordingly
179 /// * `options_ref` - [`BootdiskOptionsRef`] where advanced disk options should be saved to
180 fn fstype_on_submit(siv: &mut Cursive, fstype: &FsType, options_ref: BootdiskOptionsRef) {
181 let state = siv.user_data::<InstallerState>().unwrap();
182 let runinfo = state.runtime_info.clone();
183 let product_conf = state.setup_info.config.clone();
184
185 // Only used for LVM configurations, ZFS and Btrfs do not use the target disk selector
186 // Must be done here, as we cannot mutable borrow `siv` a second time inside the closure
187 // below.
188 let selected_lvm_disk = siv
189 .find_name::<FormView>("bootdisk-options-target-disk")
190 .and_then(|v| v.get_value::<SelectView<Disk>, _>(0))
191 // If not defined, then the view was switched from a non-LVM filesystem to a LVM one.
192 // Just use the first disk is such a case.
193 .unwrap_or_else(|| runinfo.disks[0].clone());
194
195 // Update the (inner) options view
196 siv.call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
197 if let Some(AdvancedBootdiskOptionsView { view }) =
198 view.get_content_mut().downcast_mut()
199 {
200 view.remove_child(3);
201 match fstype {
202 FsType::Ext4 | FsType::Xfs => {
203 view.add_child(LvmBootdiskOptionsView::new_with_defaults(
204 &selected_lvm_disk,
205 &product_conf,
206 ));
207 }
208 FsType::Zfs(_) => view.add_child(ZfsBootdiskOptionsView::new_with_defaults(
209 &runinfo,
210 &product_conf,
211 )),
212 FsType::Btrfs(_) => {
213 view.add_child(BtrfsBootdiskOptionsView::new_with_defaults(&runinfo.disks))
214 }
215 }
216 }
217 });
218
219 // The "bootdisk-options-target-disk" view might be either a `SelectView` (if ext4 of XFS
220 // is used) or a label containing the filesytem/RAID type (for ZFS and Btrfs).
221 // Now, unconditionally replace it with the appropriate type of these two, depending on the
222 // newly selected filesystem type.
223 siv.call_on_name(
224 "bootdisk-options-target-disk",
225 move |view: &mut FormView| match fstype {
226 FsType::Ext4 | FsType::Xfs => {
227 view.replace_child(
228 0,
229 target_bootdisk_selectview(&runinfo.disks, options_ref, &selected_lvm_disk),
230 );
231 }
232 other => view.replace_child(0, TextView::new(other.to_string())),
233 },
234 );
235 }
236
237 fn get_values(&mut self) -> Result<BootdiskOptions, String> {
238 let fstype = self
239 .view
240 .get_child(1)
241 .and_then(|v| v.downcast_ref::<FormView>())
242 .and_then(|v| v.get_value::<SelectView<FsType>, _>(0))
243 .ok_or("Failed to retrieve filesystem type".to_owned())?;
244
245 let advanced = self
246 .view
247 .get_child_mut(3)
248 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?;
249
250 if let Some(view) = advanced.downcast_mut::<LvmBootdiskOptionsView>() {
251 let (disk, advanced) = view
252 .get_values()
253 .ok_or("Failed to retrieve advanced bootdisk options")?;
254
255 Ok(BootdiskOptions {
256 disks: vec![disk],
257 fstype,
258 advanced: AdvancedBootdiskOptions::Lvm(advanced),
259 })
260 } else if let Some(view) = advanced.downcast_mut::<ZfsBootdiskOptionsView>() {
261 let (disks, advanced) = view
262 .get_values()
263 .ok_or("Failed to retrieve advanced bootdisk options")?;
264
265 if let FsType::Zfs(level) = fstype {
266 check_zfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
267 }
268
269 Ok(BootdiskOptions {
270 disks,
271 fstype,
272 advanced: AdvancedBootdiskOptions::Zfs(advanced),
273 })
274 } else if let Some(view) = advanced.downcast_mut::<BtrfsBootdiskOptionsView>() {
275 let (disks, advanced) = view
276 .get_values()
277 .ok_or("Failed to retrieve advanced bootdisk options")?;
278
279 if let FsType::Btrfs(level) = fstype {
280 check_btrfs_raid_config(level, &disks).map_err(|err| format!("{fstype}: {err}"))?;
281 }
282
283 Ok(BootdiskOptions {
284 disks,
285 fstype,
286 advanced: AdvancedBootdiskOptions::Btrfs(advanced),
287 })
288 } else {
289 Err("Invalid bootdisk view state".to_owned())
290 }
291 }
292 }
293
294 impl ViewWrapper for AdvancedBootdiskOptionsView {
295 cursive::wrap_impl!(self.view: LinearLayout);
296 }
297
298 struct LvmBootdiskOptionsView {
299 view: FormView,
300 disk: Disk,
301 has_extra_fields: bool,
302 }
303
304 impl LvmBootdiskOptionsView {
305 fn new(disk: &Disk, options: &LvmBootdiskOptions, product_conf: &ProductConfig) -> Self {
306 let show_extra_fields = product_conf.product == ProxmoxProduct::PVE;
307
308 let view = FormView::new()
309 .child(
310 "Total size",
311 DiskSizeEditView::new()
312 .content(options.total_size)
313 .max_value(options.total_size),
314 )
315 .child(
316 "Swap size",
317 DiskSizeEditView::new_emptyable().content_maybe(options.swap_size),
318 )
319 .child_conditional(
320 show_extra_fields,
321 "Maximum root volume size",
322 DiskSizeEditView::new_emptyable().content_maybe(options.max_root_size),
323 )
324 .child_conditional(
325 show_extra_fields,
326 "Maximum data volume size",
327 DiskSizeEditView::new_emptyable().content_maybe(options.max_data_size),
328 )
329 .child(
330 "Minimum free LVM space",
331 DiskSizeEditView::new_emptyable().content_maybe(options.min_lvm_free),
332 );
333
334 Self {
335 view,
336 disk: disk.clone(),
337 has_extra_fields: show_extra_fields,
338 }
339 }
340
341 fn new_with_defaults(disk: &Disk, product_conf: &ProductConfig) -> Self {
342 Self::new(disk, &LvmBootdiskOptions::defaults_from(disk), product_conf)
343 }
344
345 fn get_values(&mut self) -> Option<(Disk, LvmBootdiskOptions)> {
346 let min_lvm_free_id = if self.has_extra_fields { 4 } else { 2 };
347
348 let max_root_size = self
349 .has_extra_fields
350 .then(|| self.view.get_value::<DiskSizeEditView, _>(2))
351 .flatten();
352 let max_data_size = self
353 .has_extra_fields
354 .then(|| self.view.get_value::<DiskSizeEditView, _>(3))
355 .flatten();
356
357 Some((
358 self.disk.clone(),
359 LvmBootdiskOptions {
360 total_size: self.view.get_value::<DiskSizeEditView, _>(0)?,
361 swap_size: self.view.get_value::<DiskSizeEditView, _>(1),
362 max_root_size,
363 max_data_size,
364 min_lvm_free: self.view.get_value::<DiskSizeEditView, _>(min_lvm_free_id),
365 },
366 ))
367 }
368 }
369
370 impl ViewWrapper for LvmBootdiskOptionsView {
371 cursive::wrap_impl!(self.view: FormView);
372 }
373
374 struct MultiDiskOptionsView<T> {
375 view: LinearLayout,
376 phantom: PhantomData<T>,
377 }
378
379 impl<T: View> MultiDiskOptionsView<T> {
380 fn new(avail_disks: &[Disk], selected_disks: &[usize], options_view: T) -> Self {
381 let mut selectable_disks = avail_disks
382 .iter()
383 .map(|d| (d.to_string(), Some(d.clone())))
384 .collect::<Vec<(String, Option<Disk>)>>();
385
386 selectable_disks.push(("-- do not use --".to_owned(), None));
387
388 let mut disk_form = FormView::new();
389 for (i, _) in avail_disks.iter().enumerate() {
390 disk_form.add_child(
391 &format!("Harddisk {i}"),
392 SelectView::new()
393 .popup()
394 .with_all(selectable_disks.clone())
395 .selected(selected_disks[i]),
396 );
397 }
398
399 let mut disk_select_view = LinearLayout::vertical()
400 .child(TextView::new("Disk setup").center())
401 .child(DummyView)
402 .child(ScrollView::new(disk_form.with_name("multidisk-disk-form")));
403
404 if avail_disks.len() > 3 {
405 let do_not_use_index = selectable_disks.len() - 1;
406 let deselect_all_button = Button::new("Deselect all", move |siv| {
407 siv.call_on_name("multidisk-disk-form", |view: &mut FormView| {
408 view.call_on_childs(&|v: &mut SelectView<Option<Disk>>| {
409 // As there is no .on_select() callback defined on the
410 // SelectView's, the returned callback here can be safely
411 // ignored.
412 v.set_selection(do_not_use_index);
413 });
414 });
415 });
416
417 disk_select_view.add_child(PaddedView::lrtb(
418 0,
419 0,
420 1,
421 0,
422 LinearLayout::horizontal()
423 .child(DummyView.full_width())
424 .child(deselect_all_button),
425 ));
426 }
427
428 let options_view = LinearLayout::vertical()
429 .child(TextView::new("Advanced options").center())
430 .child(DummyView)
431 .child(options_view);
432
433 let view = LinearLayout::horizontal()
434 .child(disk_select_view)
435 .child(DummyView.fixed_width(3))
436 .child(options_view);
437
438 Self {
439 view: LinearLayout::vertical().child(view),
440 phantom: PhantomData,
441 }
442 }
443
444 fn top_panel(mut self, view: impl View) -> Self {
445 if self.has_top_panel() {
446 self.view.remove_child(0);
447 }
448
449 self.view.insert_child(0, Panel::new(view));
450 self
451 }
452
453 ///
454 /// This function returns a tuple of vectors. The first vector contains the currently selected
455 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
456 /// contains indices of each slot's selection, which enables us to restore the selection even
457 /// for empty slots.
458 ///
459 fn get_disks_and_selection(&mut self) -> Option<(Vec<Disk>, Vec<usize>)> {
460 let mut disks = vec![];
461 let view_top_index = usize::from(self.has_top_panel());
462
463 let disk_form = self
464 .view
465 .get_child_mut(view_top_index)?
466 .downcast_mut::<LinearLayout>()?
467 .get_child_mut(0)?
468 .downcast_mut::<LinearLayout>()?
469 .get_child_mut(2)?
470 .downcast_mut::<ScrollView<NamedView<FormView>>>()?
471 .get_inner_mut()
472 .get_mut();
473
474 let mut selected_disks = Vec::new();
475
476 for i in 0..disk_form.len() {
477 let disk = disk_form.get_value::<SelectView<Option<Disk>>, _>(i)?;
478
479 // `None` means no disk was selected for this slot
480 if let Some(disk) = disk {
481 disks.push(disk);
482 }
483
484 selected_disks.push(
485 disk_form
486 .get_child::<SelectView<Option<Disk>>>(i)?
487 .selected_id()?,
488 );
489 }
490
491 Some((disks, selected_disks))
492 }
493
494 fn inner_mut(&mut self) -> Option<&mut T> {
495 let view_top_index = usize::from(self.has_top_panel());
496
497 self.view
498 .get_child_mut(view_top_index)?
499 .downcast_mut::<LinearLayout>()?
500 .get_child_mut(2)?
501 .downcast_mut::<LinearLayout>()?
502 .get_child_mut(2)?
503 .downcast_mut::<T>()
504 }
505
506 fn has_top_panel(&self) -> bool {
507 // The root view should only ever have one or two children
508 assert!([1, 2].contains(&self.view.len()));
509
510 self.view.len() == 2
511 }
512 }
513
514 impl<T: 'static> ViewWrapper for MultiDiskOptionsView<T> {
515 cursive::wrap_impl!(self.view: LinearLayout);
516 }
517
518 struct BtrfsBootdiskOptionsView {
519 view: MultiDiskOptionsView<FormView>,
520 }
521
522 impl BtrfsBootdiskOptionsView {
523 fn new(disks: &[Disk], options: &BtrfsBootdiskOptions) -> Self {
524 let view = MultiDiskOptionsView::new(
525 disks,
526 &options.selected_disks,
527 FormView::new().child("hdsize", DiskSizeEditView::new().content(options.disk_size)),
528 )
529 .top_panel(TextView::new("Btrfs integration is a technology preview!").center());
530
531 Self { view }
532 }
533
534 fn new_with_defaults(disks: &[Disk]) -> Self {
535 Self::new(disks, &BtrfsBootdiskOptions::defaults_from(disks))
536 }
537
538 fn get_values(&mut self) -> Option<(Vec<Disk>, BtrfsBootdiskOptions)> {
539 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
540 let disk_size = self.view.inner_mut()?.get_value::<DiskSizeEditView, _>(0)?;
541
542 Some((
543 disks,
544 BtrfsBootdiskOptions {
545 disk_size,
546 selected_disks,
547 },
548 ))
549 }
550 }
551
552 impl ViewWrapper for BtrfsBootdiskOptionsView {
553 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
554 }
555
556 struct ZfsBootdiskOptionsView {
557 view: MultiDiskOptionsView<FormView>,
558 }
559
560 impl ZfsBootdiskOptionsView {
561 // TODO: Re-apply previous disk selection from `options` correctly
562 fn new(
563 runinfo: &RuntimeInfo,
564 options: &ZfsBootdiskOptions,
565 product_conf: &ProductConfig,
566 ) -> Self {
567 let is_pve = product_conf.product == ProxmoxProduct::PVE;
568
569 let inner = FormView::new()
570 .child("ashift", IntegerEditView::new().content(options.ashift))
571 .child(
572 "compress",
573 SelectView::new()
574 .popup()
575 .with_all(ZFS_COMPRESS_OPTIONS.iter().map(|o| (o.to_string(), *o)))
576 .selected(
577 ZFS_COMPRESS_OPTIONS
578 .iter()
579 .position(|o| *o == options.compress)
580 .unwrap_or_default(),
581 ),
582 )
583 .child(
584 "checksum",
585 SelectView::new()
586 .popup()
587 .with_all(ZFS_CHECKSUM_OPTIONS.iter().map(|o| (o.to_string(), *o)))
588 .selected(
589 ZFS_CHECKSUM_OPTIONS
590 .iter()
591 .position(|o| *o == options.checksum)
592 .unwrap_or_default(),
593 ),
594 )
595 .child("copies", IntegerEditView::new().content(options.copies).max_value(3))
596 .child_conditional(
597 is_pve,
598 "ARC max size",
599 IntegerEditView::new_with_suffix("MiB")
600 .max_value(runinfo.total_memory)
601 .content(options.arc_max),
602 )
603 .child("hdsize", DiskSizeEditView::new().content(options.disk_size));
604
605 let view = MultiDiskOptionsView::new(&runinfo.disks, &options.selected_disks, inner)
606 .top_panel(TextView::new(
607 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
608 ).center());
609
610 Self { view }
611 }
612
613 fn new_with_defaults(runinfo: &RuntimeInfo, product_conf: &ProductConfig) -> Self {
614 Self::new(
615 runinfo,
616 &ZfsBootdiskOptions::defaults_from(runinfo, product_conf),
617 product_conf,
618 )
619 }
620
621 fn get_values(&mut self) -> Option<(Vec<Disk>, ZfsBootdiskOptions)> {
622 let (disks, selected_disks) = self.view.get_disks_and_selection()?;
623 let view = self.view.inner_mut()?;
624 let has_arc_max = view.len() >= 6;
625 let disk_size_index = if has_arc_max { 5 } else { 4 };
626
627 let ashift = view.get_value::<IntegerEditView, _>(0)?;
628 let compress = view.get_value::<SelectView<_>, _>(1)?;
629 let checksum = view.get_value::<SelectView<_>, _>(2)?;
630 let copies = view.get_value::<IntegerEditView, _>(3)?;
631 let disk_size = view.get_value::<DiskSizeEditView, _>(disk_size_index)?;
632
633 let arc_max = if has_arc_max {
634 view.get_value::<IntegerEditView, _>(4)?
635 .max(ZFS_ARC_MIN_SIZE_MIB)
636 } else {
637 0 // use built-in ZFS default value
638 };
639
640 Some((
641 disks,
642 ZfsBootdiskOptions {
643 ashift,
644 compress,
645 checksum,
646 copies,
647 arc_max,
648 disk_size,
649 selected_disks,
650 },
651 ))
652 }
653 }
654
655 impl ViewWrapper for ZfsBootdiskOptionsView {
656 cursive::wrap_impl!(self.view: MultiDiskOptionsView<FormView>);
657 }
658
659 fn advanced_options_view(
660 runinfo: &RuntimeInfo,
661 options_ref: BootdiskOptionsRef,
662 product_conf: ProductConfig,
663 ) -> impl View {
664 Dialog::around(AdvancedBootdiskOptionsView::new(
665 runinfo,
666 options_ref.clone(),
667 product_conf,
668 ))
669 .title("Advanced bootdisk options")
670 .button("Ok", {
671 move |siv| {
672 let options = siv
673 .call_on_name("advanced-bootdisk-options-dialog", |view: &mut Dialog| {
674 view.get_content_mut()
675 .downcast_mut::<AdvancedBootdiskOptionsView>()
676 .map(AdvancedBootdiskOptionsView::get_values)
677 })
678 .flatten();
679
680 let options = match options {
681 Some(Ok(options)) => options,
682 Some(Err(err)) => {
683 siv.add_layer(Dialog::info(err));
684 return;
685 }
686 None => {
687 siv.add_layer(Dialog::info("Failed to retrieve bootdisk options view"));
688 return;
689 }
690 };
691
692 if let Err(duplicate) = check_for_duplicate_disks(&options.disks) {
693 siv.add_layer(Dialog::info(format!(
694 "Cannot select same disk twice: {duplicate}"
695 )));
696 return;
697 }
698
699 siv.pop_layer();
700 *(*options_ref).borrow_mut() = options;
701 }
702 })
703 .with_name("advanced-bootdisk-options-dialog")
704 .max_size((120, 40))
705 }
706
707 /// Creates a select view for all disks specified.
708 ///
709 /// # Arguments
710 ///
711 /// * `avail_disks` - Disks that should be shown in the select view
712 /// * `options_ref` - [`BootdiskOptionsRef`] where advanced disk options should be saved to
713 /// * `selected_disk` - Optional, specifies which disk should be pre-selected
714 fn target_bootdisk_selectview(
715 avail_disks: &[Disk],
716 options_ref: BootdiskOptionsRef,
717 selected_disk: &Disk,
718 ) -> SelectView<Disk> {
719 let selected_disk_pos = avail_disks
720 .iter()
721 .position(|d| d.index == selected_disk.index)
722 .unwrap_or_default();
723
724 SelectView::new()
725 .popup()
726 .with_all(avail_disks.iter().map(|d| (d.to_string(), d.clone())))
727 .selected(selected_disk_pos)
728 .on_submit(move |_, disk| {
729 options_ref.borrow_mut().disks = vec![disk.clone()];
730 options_ref.borrow_mut().advanced =
731 AdvancedBootdiskOptions::Lvm(LvmBootdiskOptions::defaults_from(disk));
732 })
733 }