1 use std
::{cell::RefCell, collections::HashSet, marker::PhantomData, rc::Rc}
;
4 view
::{Nameable, Resizable, ViewWrapper}
,
6 Button
, Dialog
, DummyView
, LinearLayout
, NamedView
, PaddedView
, Panel
, ScrollView
,
12 use super::{DiskSizeEditView, FormView, IntegerEditView}
;
14 AdvancedBootdiskOptions
, BootdiskOptions
, BtrfsBootdiskOptions
, Disk
, FsType
,
15 LvmBootdiskOptions
, ZfsBootdiskOptions
, FS_TYPES
, ZFS_CHECKSUM_OPTIONS
, ZFS_COMPRESS_OPTIONS
,
17 use crate::setup
::ProxmoxProduct
;
19 pub struct BootdiskOptionsView
{
21 advanced_options
: Rc
<RefCell
<BootdiskOptions
>>,
24 impl BootdiskOptionsView
{
25 pub fn new(disks
: &[Disk
], options
: &BootdiskOptions
) -> Self {
26 let bootdisk_form
= FormView
::new()
31 .with_all(disks
.iter().map(|d
| (d
.to_string(), d
.clone()))),
33 .with_name("bootdisk-options-target-disk");
35 let advanced_options
= Rc
::new(RefCell
::new(options
.clone()));
37 let advanced_button
= LinearLayout
::horizontal()
38 .child(DummyView
.full_width())
39 .child(Button
::new("Advanced options", {
40 let disks
= disks
.to_owned();
41 let options
= advanced_options
.clone();
43 siv
.add_layer(advanced_options_view(&disks
, options
.clone()));
47 let view
= LinearLayout
::vertical()
50 .child(advanced_button
);
58 pub fn get_values(&mut self) -> Result
<BootdiskOptions
, String
> {
59 let mut options
= (*self.advanced_options
).clone().into_inner();
61 if [FsType
::Ext4
, FsType
::Xfs
].contains(&options
.fstype
) {
65 .and_then(|v
| v
.downcast_mut
::<NamedView
<FormView
>>())
66 .map(NamedView
::<FormView
>::get_mut
)
67 .and_then(|v
| v
.get_value
::<SelectView
<Disk
>, _
>(0))
68 .ok_or("failed to retrieve bootdisk")?
;
70 options
.disks
= vec
![disk
];
77 impl ViewWrapper
for BootdiskOptionsView
{
78 cursive
::wrap_impl
!(self.view
: LinearLayout
);
81 struct AdvancedBootdiskOptionsView
{
85 impl AdvancedBootdiskOptionsView
{
86 fn new(disks
: &[Disk
], options
: &BootdiskOptions
) -> Self {
87 let enable_btrfs
= crate::setup_info().config
.enable_btrfs
;
89 let filter_btrfs
= |fstype
: &&FsType
| -> bool { enable_btrfs || !fstype.is_btrfs() }
;
91 let fstype_select
= SelectView
::new()
97 .map(|t
| (t
.to_string(), *t
)),
102 .filter(filter_btrfs
)
103 .position(|t
| *t
== options
.fstype
)
104 .unwrap_or_default(),
107 let disks
= disks
.to_owned();
108 move |siv
, fstype
| Self::fstype_on_submit(siv
, &disks
, fstype
)
111 let mut view
= LinearLayout
::vertical()
112 .child(DummyView
.full_width())
113 .child(FormView
::new().child("Filesystem", fstype_select
))
114 .child(DummyView
.full_width());
116 match &options
.advanced
{
117 AdvancedBootdiskOptions
::Lvm(lvm
) => view
.add_child(LvmBootdiskOptionsView
::new(lvm
)),
118 AdvancedBootdiskOptions
::Zfs(zfs
) => {
119 view
.add_child(ZfsBootdiskOptionsView
::new(disks
, zfs
))
121 AdvancedBootdiskOptions
::Btrfs(btrfs
) => {
122 view
.add_child(BtrfsBootdiskOptionsView
::new(disks
, btrfs
))
129 fn fstype_on_submit(siv
: &mut Cursive
, disks
: &[Disk
], fstype
: &FsType
) {
130 siv
.call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
131 if let Some(AdvancedBootdiskOptionsView { view }
) =
132 view
.get_content_mut().downcast_mut()
134 view
.remove_child(3);
136 FsType
::Ext4
| FsType
::Xfs
=> view
.add_child(LvmBootdiskOptionsView
::new(
137 &LvmBootdiskOptions
::defaults_from(&disks
[0]),
139 FsType
::Zfs(_
) => view
.add_child(ZfsBootdiskOptionsView
::new(
141 &ZfsBootdiskOptions
::defaults_from(disks
),
143 FsType
::Btrfs(_
) => view
.add_child(BtrfsBootdiskOptionsView
::new(
145 &BtrfsBootdiskOptions
::defaults_from(disks
),
152 "bootdisk-options-target-disk",
153 |view
: &mut FormView
| match fstype
{
154 FsType
::Ext4
| FsType
::Xfs
=> {
159 .with_all(disks
.iter().map(|d
| (d
.to_string(), d
.clone()))),
162 other
=> view
.replace_child(0, TextView
::new(other
.to_string())),
167 fn get_values(&mut self) -> Result
<BootdiskOptions
, String
> {
171 .and_then(|v
| v
.downcast_ref
::<FormView
>())
172 .and_then(|v
| v
.get_value
::<SelectView
<FsType
>, _
>(0))
173 .ok_or("Failed to retrieve filesystem type".to_owned())?
;
178 .ok_or("Failed to retrieve advanced bootdisk options view".to_owned())?
;
180 if let Some(view
) = advanced
.downcast_mut
::<LvmBootdiskOptionsView
>() {
183 .map(AdvancedBootdiskOptions
::Lvm
)
184 .ok_or("Failed to retrieve advanced bootdisk options")?
;
191 } else if let Some(view
) = advanced
.downcast_mut
::<ZfsBootdiskOptionsView
>() {
192 let (disks
, advanced
) = view
194 .ok_or("Failed to retrieve advanced bootdisk options")?
;
199 advanced
: AdvancedBootdiskOptions
::Zfs(advanced
),
201 } else if let Some(view
) = advanced
.downcast_mut
::<BtrfsBootdiskOptionsView
>() {
202 let (disks
, advanced
) = view
204 .ok_or("Failed to retrieve advanced bootdisk options")?
;
209 advanced
: AdvancedBootdiskOptions
::Btrfs(advanced
),
212 Err("Invalid bootdisk view state".to_owned())
217 impl ViewWrapper
for AdvancedBootdiskOptionsView
{
218 cursive
::wrap_impl
!(self.view
: LinearLayout
);
221 struct LvmBootdiskOptionsView
{
225 impl LvmBootdiskOptionsView
{
226 fn new(options
: &LvmBootdiskOptions
) -> Self {
227 let is_pve
= crate::setup_info().config
.product
== ProxmoxProduct
::PVE
;
228 // TODO: Set maximum accordingly to disk size
229 let view
= FormView
::new()
232 DiskSizeEditView
::new()
233 .content(options
.total_size
)
234 .max_value(options
.total_size
),
238 DiskSizeEditView
::new_emptyable().content_maybe(options
.swap_size
),
242 "Maximum root volume size",
243 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_root_size
),
247 "Maximum data volume size",
248 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_data_size
),
251 "Minimum free LVM space",
252 DiskSizeEditView
::new_emptyable().content_maybe(options
.min_lvm_free
),
258 fn get_values(&mut self) -> Option
<LvmBootdiskOptions
> {
259 let is_pve
= crate::setup_info().config
.product
== ProxmoxProduct
::PVE
;
260 let min_lvm_free_id
= if is_pve { 4 }
else { 2 }
;
261 let max_root_size
= if is_pve
{
262 self.view
.get_value
::<DiskSizeEditView
, _
>(2)
266 let max_data_size
= if is_pve
{
267 self.view
.get_value
::<DiskSizeEditView
, _
>(3)
271 Some(LvmBootdiskOptions
{
272 total_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(0)?
,
273 swap_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(1),
276 min_lvm_free
: self.view
.get_value
::<DiskSizeEditView
, _
>(min_lvm_free_id
),
281 impl ViewWrapper
for LvmBootdiskOptionsView
{
282 cursive
::wrap_impl
!(self.view
: FormView
);
285 struct MultiDiskOptionsView
<T
> {
287 phantom
: PhantomData
<T
>,
290 impl<T
: View
> MultiDiskOptionsView
<T
> {
291 fn new(avail_disks
: &[Disk
], selected_disks
: &[usize], options_view
: T
) -> Self {
292 let mut selectable_disks
= avail_disks
294 .map(|d
| (d
.to_string(), Some(d
.clone())))
295 .collect
::<Vec
<(String
, Option
<Disk
>)>>();
297 selectable_disks
.push(("-- do not use --".to_owned(), None
));
299 let mut disk_form
= FormView
::new();
300 for (i
, _
) in avail_disks
.iter().enumerate() {
302 &format
!("Harddisk {i}"),
305 .with_all(selectable_disks
.clone())
306 .selected(selected_disks
[i
]),
310 let mut disk_select_view
= LinearLayout
::vertical()
311 .child(TextView
::new("Disk setup").center())
313 .child(ScrollView
::new(disk_form
.with_name("multidisk-disk-form")));
315 if avail_disks
.len() > 3 {
316 let do_not_use_index
= selectable_disks
.len() - 1;
317 let deselect_all_button
= Button
::new("Deselect all", move |siv
| {
318 siv
.call_on_name("multidisk-disk-form", |view
: &mut FormView
| {
319 view
.call_on_childs(&|v
: &mut SelectView
<Option
<Disk
>>| {
320 // As there is no .on_select() callback defined on the
321 // SelectView's, the returned callback here can be safely
323 v
.set_selection(do_not_use_index
);
328 disk_select_view
.add_child(PaddedView
::lrtb(
333 LinearLayout
::horizontal()
334 .child(DummyView
.full_width())
335 .child(deselect_all_button
),
339 let options_view
= LinearLayout
::vertical()
340 .child(TextView
::new("Advanced options").center())
342 .child(options_view
);
344 let view
= LinearLayout
::horizontal()
345 .child(disk_select_view
)
346 .child(DummyView
.fixed_width(3))
347 .child(options_view
);
350 view
: LinearLayout
::vertical().child(view
),
351 phantom
: PhantomData
,
355 fn top_panel(mut self, view
: impl View
) -> Self {
356 if self.has_top_panel() {
357 self.view
.remove_child(0);
360 self.view
.insert_child(0, Panel
::new(view
));
365 /// This function returns a tuple of vectors. The first vector contains the currently selected
366 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
367 /// contains indices of each slot's selection, which enables us to restore the selection even
370 fn get_disks_and_selection(&mut self) -> Option
<(Vec
<Disk
>, Vec
<usize>)> {
371 let mut disks
= vec
![];
372 let view_top_index
= usize::from(self.has_top_panel());
376 .get_child_mut(view_top_index
)?
377 .downcast_mut
::<LinearLayout
>()?
379 .downcast_mut
::<LinearLayout
>()?
381 .downcast_mut
::<ScrollView
<NamedView
<FormView
>>>()?
385 let mut selected_disks
= Vec
::new();
387 for i
in 0..disk_form
.len() {
388 let disk
= disk_form
.get_value
::<SelectView
<Option
<Disk
>>, _
>(i
)?
;
390 // `None` means no disk was selected for this slot
391 if let Some(disk
) = disk
{
397 .get_child
::<SelectView
<Option
<Disk
>>>(i
)?
402 Some((disks
, selected_disks
))
405 fn inner_mut(&mut self) -> Option
<&mut T
> {
406 let view_top_index
= usize::from(self.has_top_panel());
409 .get_child_mut(view_top_index
)?
410 .downcast_mut
::<LinearLayout
>()?
412 .downcast_mut
::<LinearLayout
>()?
417 fn has_top_panel(&self) -> bool
{
418 // The root view should only ever have one or two children
419 assert
!([1, 2].contains(&self.view
.len()));
425 impl<T
: '
static> ViewWrapper
for MultiDiskOptionsView
<T
> {
426 cursive
::wrap_impl
!(self.view
: LinearLayout
);
429 struct BtrfsBootdiskOptionsView
{
430 view
: MultiDiskOptionsView
<FormView
>,
433 impl BtrfsBootdiskOptionsView
{
434 fn new(disks
: &[Disk
], options
: &BtrfsBootdiskOptions
) -> Self {
435 let view
= MultiDiskOptionsView
::new(
437 &options
.selected_disks
,
438 FormView
::new().child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
)),
440 .top_panel(TextView
::new("Btrfs integration is a technology preview!").center());
445 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, BtrfsBootdiskOptions
)> {
446 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
447 let disk_size
= self.view
.inner_mut()?
.get_value
::<DiskSizeEditView
, _
>(0)?
;
451 BtrfsBootdiskOptions
{
459 impl ViewWrapper
for BtrfsBootdiskOptionsView
{
460 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
463 struct ZfsBootdiskOptionsView
{
464 view
: MultiDiskOptionsView
<FormView
>,
467 impl ZfsBootdiskOptionsView
{
468 // TODO: Re-apply previous disk selection from `options` correctly
469 fn new(disks
: &[Disk
], options
: &ZfsBootdiskOptions
) -> Self {
470 let inner
= FormView
::new()
471 .child("ashift", IntegerEditView
::new().content(options
.ashift
))
476 .with_all(ZFS_COMPRESS_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
480 .position(|o
| *o
== options
.compress
)
481 .unwrap_or_default(),
488 .with_all(ZFS_CHECKSUM_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
492 .position(|o
| *o
== options
.checksum
)
493 .unwrap_or_default(),
496 .child("copies", IntegerEditView
::new().content(options
.copies
))
497 .child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
));
499 let view
= MultiDiskOptionsView
::new(disks
, &options
.selected_disks
, inner
)
500 .top_panel(TextView
::new(
501 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
507 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, ZfsBootdiskOptions
)> {
508 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
509 let view
= self.view
.inner_mut()?
;
511 let ashift
= view
.get_value
::<IntegerEditView
, _
>(0)?
;
512 let compress
= view
.get_value
::<SelectView
<_
>, _
>(1)?
;
513 let checksum
= view
.get_value
::<SelectView
<_
>, _
>(2)?
;
514 let copies
= view
.get_value
::<IntegerEditView
, _
>(3)?
;
515 let disk_size
= view
.get_value
::<DiskSizeEditView
, _
>(4)?
;
531 impl ViewWrapper
for ZfsBootdiskOptionsView
{
532 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
535 fn advanced_options_view(disks
: &[Disk
], options
: Rc
<RefCell
<BootdiskOptions
>>) -> impl View
{
536 Dialog
::around(AdvancedBootdiskOptionsView
::new(
538 &(*options
).borrow(),
540 .title("Advanced bootdisk options")
542 let options_ref
= options
.clone();
545 .call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
546 view
.get_content_mut()
548 .map(AdvancedBootdiskOptionsView
::get_values
)
552 let options
= match options
{
553 Some(Ok(options
)) => options
,
555 siv
.add_layer(Dialog
::info(err
));
559 siv
.add_layer(Dialog
::info("Failed to retrieve bootdisk options view"));
564 if let Err(duplicate
) = check_for_duplicate_disks(&options
.disks
) {
565 siv
.add_layer(Dialog
::info(format
!(
566 "Cannot select same disk twice: {duplicate}"
572 *(*options_ref
).borrow_mut() = options
;
575 .with_name("advanced-bootdisk-options-dialog")
579 /// Checks a list of disks for duplicate entries, using their index as key.
583 /// * `disks` - A list of disks to check for duplicates.
584 fn check_for_duplicate_disks(disks
: &[Disk
]) -> Result
<(), &Disk
> {
585 let mut set
= HashSet
::new();
588 if !set
.insert(&disk
.index
) {