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) -> Option
<BootdiskOptions
> {
171 .downcast_ref
::<FormView
>()?
172 .get_value
::<SelectView
<FsType
>, _
>(0)?
;
174 let advanced
= self.view
.get_child_mut(3)?
;
176 if let Some(view
) = advanced
.downcast_mut
::<LvmBootdiskOptionsView
>() {
177 Some(BootdiskOptions
{
180 advanced
: view
.get_values().map(AdvancedBootdiskOptions
::Lvm
)?
,
182 } else if let Some(view
) = advanced
.downcast_mut
::<ZfsBootdiskOptionsView
>() {
183 let (disks
, advanced
) = view
.get_values()?
;
185 Some(BootdiskOptions
{
188 advanced
: AdvancedBootdiskOptions
::Zfs(advanced
),
190 } else if let Some(view
) = advanced
.downcast_mut
::<BtrfsBootdiskOptionsView
>() {
191 let (disks
, advanced
) = view
.get_values()?
;
193 Some(BootdiskOptions
{
196 advanced
: AdvancedBootdiskOptions
::Btrfs(advanced
),
204 impl ViewWrapper
for AdvancedBootdiskOptionsView
{
205 cursive
::wrap_impl
!(self.view
: LinearLayout
);
208 struct LvmBootdiskOptionsView
{
212 impl LvmBootdiskOptionsView
{
213 fn new(options
: &LvmBootdiskOptions
) -> Self {
214 let is_pve
= crate::setup_info().config
.product
== ProxmoxProduct
::PVE
;
215 // TODO: Set maximum accordingly to disk size
216 let view
= FormView
::new()
219 DiskSizeEditView
::new()
220 .content(options
.total_size
)
221 .max_value(options
.total_size
),
225 DiskSizeEditView
::new_emptyable().content_maybe(options
.swap_size
),
229 "Maximum root volume size",
230 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_root_size
),
234 "Maximum data volume size",
235 DiskSizeEditView
::new_emptyable().content_maybe(options
.max_data_size
),
238 "Minimum free LVM space",
239 DiskSizeEditView
::new_emptyable().content_maybe(options
.min_lvm_free
),
245 fn get_values(&mut self) -> Option
<LvmBootdiskOptions
> {
246 let is_pve
= crate::setup_info().config
.product
== ProxmoxProduct
::PVE
;
247 let min_lvm_free_id
= if is_pve { 4 }
else { 2 }
;
248 let max_root_size
= if is_pve
{
249 self.view
.get_value
::<DiskSizeEditView
, _
>(2)
253 let max_data_size
= if is_pve
{
254 self.view
.get_value
::<DiskSizeEditView
, _
>(3)
258 Some(LvmBootdiskOptions
{
259 total_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(0)?
,
260 swap_size
: self.view
.get_value
::<DiskSizeEditView
, _
>(1),
263 min_lvm_free
: self.view
.get_value
::<DiskSizeEditView
, _
>(min_lvm_free_id
),
268 impl ViewWrapper
for LvmBootdiskOptionsView
{
269 cursive
::wrap_impl
!(self.view
: FormView
);
272 struct MultiDiskOptionsView
<T
> {
274 phantom
: PhantomData
<T
>,
277 impl<T
: View
> MultiDiskOptionsView
<T
> {
278 fn new(avail_disks
: &[Disk
], selected_disks
: &[usize], options_view
: T
) -> Self {
279 let mut selectable_disks
= avail_disks
281 .map(|d
| (d
.to_string(), Some(d
.clone())))
282 .collect
::<Vec
<(String
, Option
<Disk
>)>>();
284 selectable_disks
.push(("-- do not use --".to_owned(), None
));
286 let mut disk_form
= FormView
::new();
287 for (i
, _
) in avail_disks
.iter().enumerate() {
289 &format
!("Harddisk {i}"),
292 .with_all(selectable_disks
.clone())
293 .selected(selected_disks
[i
]),
297 let mut disk_select_view
= LinearLayout
::vertical()
298 .child(TextView
::new("Disk setup").center())
300 .child(ScrollView
::new(disk_form
.with_name("multidisk-disk-form")));
302 if avail_disks
.len() > 3 {
303 let do_not_use_index
= selectable_disks
.len() - 1;
304 let deselect_all_button
= Button
::new("Deselect all", move |siv
| {
305 siv
.call_on_name("multidisk-disk-form", |view
: &mut FormView
| {
306 view
.call_on_childs(&|v
: &mut SelectView
<Option
<Disk
>>| {
307 // As there is no .on_select() callback defined on the
308 // SelectView's, the returned callback here can be safely
310 v
.set_selection(do_not_use_index
);
315 disk_select_view
.add_child(PaddedView
::lrtb(
320 LinearLayout
::horizontal()
321 .child(DummyView
.full_width())
322 .child(deselect_all_button
),
326 let options_view
= LinearLayout
::vertical()
327 .child(TextView
::new("Advanced options").center())
329 .child(options_view
);
331 let view
= LinearLayout
::horizontal()
332 .child(disk_select_view
)
333 .child(DummyView
.fixed_width(3))
334 .child(options_view
);
337 view
: LinearLayout
::vertical().child(view
),
338 phantom
: PhantomData
,
342 fn top_panel(mut self, view
: impl View
) -> Self {
343 if self.has_top_panel() {
344 self.view
.remove_child(0);
347 self.view
.insert_child(0, Panel
::new(view
));
352 /// This function returns a tuple of vectors. The first vector contains the currently selected
353 /// disks in order of their selection slot. Empty slots are filtered out. The second vector
354 /// contains indices of each slot's selection, which enables us to restore the selection even
357 fn get_disks_and_selection(&mut self) -> Option
<(Vec
<Disk
>, Vec
<usize>)> {
358 let mut disks
= vec
![];
359 let view_top_index
= usize::from(self.has_top_panel());
363 .get_child_mut(view_top_index
)?
364 .downcast_mut
::<LinearLayout
>()?
366 .downcast_mut
::<LinearLayout
>()?
368 .downcast_mut
::<ScrollView
<NamedView
<FormView
>>>()?
372 let mut selected_disks
= Vec
::new();
374 for i
in 0..disk_form
.len() {
375 let disk
= disk_form
.get_value
::<SelectView
<Option
<Disk
>>, _
>(i
)?
;
377 // `None` means no disk was selected for this slot
378 if let Some(disk
) = disk
{
384 .get_child
::<SelectView
<Option
<Disk
>>>(i
)?
389 Some((disks
, selected_disks
))
392 fn inner_mut(&mut self) -> Option
<&mut T
> {
393 let view_top_index
= usize::from(self.has_top_panel());
396 .get_child_mut(view_top_index
)?
397 .downcast_mut
::<LinearLayout
>()?
399 .downcast_mut
::<LinearLayout
>()?
404 fn has_top_panel(&self) -> bool
{
405 // The root view should only ever have one or two children
406 assert
!([1, 2].contains(&self.view
.len()));
412 impl<T
: '
static> ViewWrapper
for MultiDiskOptionsView
<T
> {
413 cursive
::wrap_impl
!(self.view
: LinearLayout
);
416 struct BtrfsBootdiskOptionsView
{
417 view
: MultiDiskOptionsView
<FormView
>,
420 impl BtrfsBootdiskOptionsView
{
421 fn new(disks
: &[Disk
], options
: &BtrfsBootdiskOptions
) -> Self {
422 let view
= MultiDiskOptionsView
::new(
424 &options
.selected_disks
,
425 FormView
::new().child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
)),
427 .top_panel(TextView
::new("Btrfs integration is a technology preview!").center());
432 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, BtrfsBootdiskOptions
)> {
433 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
434 let disk_size
= self.view
.inner_mut()?
.get_value
::<DiskSizeEditView
, _
>(0)?
;
438 BtrfsBootdiskOptions
{
446 impl ViewWrapper
for BtrfsBootdiskOptionsView
{
447 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
450 struct ZfsBootdiskOptionsView
{
451 view
: MultiDiskOptionsView
<FormView
>,
454 impl ZfsBootdiskOptionsView
{
455 // TODO: Re-apply previous disk selection from `options` correctly
456 fn new(disks
: &[Disk
], options
: &ZfsBootdiskOptions
) -> Self {
457 let inner
= FormView
::new()
458 .child("ashift", IntegerEditView
::new().content(options
.ashift
))
463 .with_all(ZFS_COMPRESS_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
467 .position(|o
| *o
== options
.compress
)
468 .unwrap_or_default(),
475 .with_all(ZFS_CHECKSUM_OPTIONS
.iter().map(|o
| (o
.to_string(), *o
)))
479 .position(|o
| *o
== options
.checksum
)
480 .unwrap_or_default(),
483 .child("copies", IntegerEditView
::new().content(options
.copies
))
484 .child("hdsize", DiskSizeEditView
::new().content(options
.disk_size
));
486 let view
= MultiDiskOptionsView
::new(disks
, &options
.selected_disks
, inner
)
487 .top_panel(TextView
::new(
488 "ZFS is not compatible with hardware RAID controllers, for details see the documentation."
494 fn get_values(&mut self) -> Option
<(Vec
<Disk
>, ZfsBootdiskOptions
)> {
495 let (disks
, selected_disks
) = self.view
.get_disks_and_selection()?
;
496 let view
= self.view
.inner_mut()?
;
498 let ashift
= view
.get_value
::<IntegerEditView
, _
>(0)?
;
499 let compress
= view
.get_value
::<SelectView
<_
>, _
>(1)?
;
500 let checksum
= view
.get_value
::<SelectView
<_
>, _
>(2)?
;
501 let copies
= view
.get_value
::<IntegerEditView
, _
>(3)?
;
502 let disk_size
= view
.get_value
::<DiskSizeEditView
, _
>(4)?
;
518 impl ViewWrapper
for ZfsBootdiskOptionsView
{
519 cursive
::wrap_impl
!(self.view
: MultiDiskOptionsView
<FormView
>);
522 fn advanced_options_view(disks
: &[Disk
], options
: Rc
<RefCell
<BootdiskOptions
>>) -> impl View
{
523 Dialog
::around(AdvancedBootdiskOptionsView
::new(
525 &(*options
).borrow(),
527 .title("Advanced bootdisk options")
529 let options_ref
= options
.clone();
532 .call_on_name("advanced-bootdisk-options-dialog", |view
: &mut Dialog
| {
533 view
.get_content_mut()
535 .and_then(AdvancedBootdiskOptionsView
::get_values
)
539 if let Err(duplicate
) = check_for_duplicate_disks(&options
.disks
) {
540 siv
.add_layer(Dialog
::info(format
!(
541 "Cannot select same disk twice: {duplicate}"
547 if let Some(options
) = options
{
548 *(*options_ref
).borrow_mut() = options
;
552 .with_name("advanced-bootdisk-options-dialog")
556 /// Checks a list of disks for duplicate entries, using their index as key.
560 /// * `disks` - A list of disks to check for duplicates.
561 fn check_for_duplicate_disks(disks
: &[Disk
]) -> Result
<(), &Disk
> {
562 let mut set
= HashSet
::new();
565 if !set
.insert(&disk
.index
) {