]> git.proxmox.com Git - pve-installer.git/blob - proxmox-tui-installer/src/views/mod.rs
tui: bootdisk: expose `arc_max` ZFS option for PVE installations
[pve-installer.git] / proxmox-tui-installer / src / views / mod.rs
1 use std::{net::IpAddr, rc::Rc, str::FromStr};
2
3 use cursive::{
4 event::{Event, EventResult},
5 view::{Resizable, ViewWrapper},
6 views::{EditView, LinearLayout, NamedView, ResizedView, SelectView, TextView},
7 Rect, Vec2, View,
8 };
9
10 use proxmox_installer_common::utils::CidrAddress;
11
12 mod bootdisk;
13 pub use bootdisk::*;
14
15 mod table_view;
16 pub use table_view::*;
17
18 mod timezone;
19 pub use timezone::*;
20
21 pub struct NumericEditView<T> {
22 view: LinearLayout,
23 max_value: Option<T>,
24 max_content_width: Option<usize>,
25 allow_empty: bool,
26 }
27
28 impl<T: Copy + ToString + FromStr + PartialOrd> NumericEditView<T> {
29 /// Creates a new [`NumericEditView`], with the value set to `0`.
30 pub fn new() -> Self {
31 let view = LinearLayout::horizontal().child(EditView::new().content("0").full_width());
32
33 Self {
34 view,
35 max_value: None,
36 max_content_width: None,
37 allow_empty: false,
38 }
39 }
40
41 /// Creates a new [`NumericEditView`], with the value set to `0` and a label to the right of it
42 /// with the given content, separated by a space.
43 ///
44 /// # Arguments
45 /// * `suffix` - Content for the label to the right of it.
46 pub fn new_with_suffix(suffix: &str) -> Self {
47 let view = LinearLayout::horizontal()
48 .child(EditView::new().content("0").full_width())
49 .child(TextView::new(format!(" {suffix}")));
50
51 Self {
52 view,
53 max_value: None,
54 max_content_width: None,
55 allow_empty: false,
56 }
57 }
58
59 pub fn max_value(mut self, max: T) -> Self {
60 self.max_value = Some(max);
61 self
62 }
63
64 pub fn max_content_width(mut self, width: usize) -> Self {
65 self.max_content_width = Some(width);
66 self.inner_mut().set_max_content_width(Some(width));
67 self
68 }
69
70 pub fn allow_empty(mut self, value: bool) -> Self {
71 self.allow_empty = value;
72
73 if value {
74 *self.inner_mut() = EditView::new();
75 } else {
76 *self.inner_mut() = EditView::new().content("0");
77 }
78
79 let max_content_width = self.max_content_width;
80 self.inner_mut().set_max_content_width(max_content_width);
81 self
82 }
83
84 pub fn get_content(&self) -> Result<T, <T as FromStr>::Err> {
85 assert!(!self.allow_empty);
86 self.inner().get_content().parse()
87 }
88
89 pub fn get_content_maybe(&self) -> Option<Result<T, <T as FromStr>::Err>> {
90 let content = self.inner().get_content();
91 if !content.is_empty() {
92 Some(self.inner().get_content().parse())
93 } else {
94 None
95 }
96 }
97
98 pub fn set_max_value(&mut self, max: T) {
99 self.max_value = Some(max);
100 }
101
102 fn check_bounds(&mut self, original: Rc<String>, result: EventResult) -> EventResult {
103 // Check if the new value is actually valid according to the max value, if set
104 if let Some(max) = self.max_value {
105 if let Ok(val) = self.get_content() {
106 if result.is_consumed() && val > max {
107 // Restore the original value, before the insert
108 let cb = self.inner_mut().set_content((*original).clone());
109 return EventResult::with_cb_once(move |siv| {
110 result.process(siv);
111 cb(siv);
112 });
113 }
114 }
115 }
116
117 result
118 }
119
120 /// Provides an immutable reference to the inner [`EditView`].
121 fn inner(&self) -> &EditView {
122 // Safety: Invariant; first child must always exist and be a `EditView`
123 self.view
124 .get_child(0)
125 .unwrap()
126 .downcast_ref::<ResizedView<EditView>>()
127 .unwrap()
128 .get_inner()
129 }
130
131 /// Provides a mutable reference to the inner [`EditView`].
132 fn inner_mut(&mut self) -> &mut EditView {
133 // Safety: Invariant; first child must always exist and be a `EditView`
134 self.view
135 .get_child_mut(0)
136 .unwrap()
137 .downcast_mut::<ResizedView<EditView>>()
138 .unwrap()
139 .get_inner_mut()
140 }
141
142 /// Sets the content of the inner [`EditView`]. This correctly swaps out the content without
143 /// modifying the [`EditView`] in any way.
144 ///
145 /// Chainable variant.
146 ///
147 /// # Arguments
148 /// * `content` - New, stringified content for the inner [`EditView`]. Must be a valid value
149 /// according to the containet type `T`.
150 fn content_inner(mut self, content: &str) -> Self {
151 let mut inner = EditView::new();
152 std::mem::swap(self.inner_mut(), &mut inner);
153 inner = inner.content(content);
154 std::mem::swap(self.inner_mut(), &mut inner);
155 self
156 }
157 }
158
159 pub type FloatEditView = NumericEditView<f64>;
160 pub type IntegerEditView = NumericEditView<usize>;
161
162 impl ViewWrapper for FloatEditView {
163 cursive::wrap_impl!(self.view: LinearLayout);
164
165 fn wrap_on_event(&mut self, event: Event) -> EventResult {
166 let original = self.inner_mut().get_content();
167
168 let has_decimal_place = original.find('.').is_some();
169
170 let result = match event {
171 Event::Char(c) if !c.is_numeric() && c != '.' => return EventResult::consumed(),
172 Event::Char('.') if has_decimal_place => return EventResult::consumed(),
173 _ => self.view.on_event(event),
174 };
175
176 let decimal_places = self
177 .inner_mut()
178 .get_content()
179 .split_once('.')
180 .map(|(_, s)| s.len())
181 .unwrap_or_default();
182 if decimal_places > 2 {
183 let cb = self.inner_mut().set_content((*original).clone());
184 return EventResult::with_cb_once(move |siv| {
185 result.process(siv);
186 cb(siv);
187 });
188 }
189
190 self.check_bounds(original, result)
191 }
192 }
193
194 impl FloatEditView {
195 /// Sets the value of the [`FloatEditView`].
196 pub fn content(self, content: f64) -> Self {
197 self.content_inner(&format!("{:.2}", content))
198 }
199 }
200
201 impl ViewWrapper for IntegerEditView {
202 cursive::wrap_impl!(self.view: LinearLayout);
203
204 fn wrap_on_event(&mut self, event: Event) -> EventResult {
205 let original = self.inner_mut().get_content();
206
207 let result = match event {
208 // Drop all other characters than numbers; allow dots if not set to integer-only
209 Event::Char(c) if !c.is_numeric() => EventResult::consumed(),
210 _ => self.view.on_event(event),
211 };
212
213 self.check_bounds(original, result)
214 }
215 }
216
217 impl IntegerEditView {
218 /// Sets the value of the [`IntegerEditView`].
219 pub fn content(self, content: usize) -> Self {
220 self.content_inner(&content.to_string())
221 }
222 }
223
224 pub struct DiskSizeEditView {
225 view: LinearLayout,
226 allow_empty: bool,
227 }
228
229 impl DiskSizeEditView {
230 pub fn new() -> Self {
231 let view = LinearLayout::horizontal()
232 .child(FloatEditView::new().full_width())
233 .child(TextView::new(" GB"));
234
235 Self {
236 view,
237 allow_empty: false,
238 }
239 }
240
241 pub fn new_emptyable() -> Self {
242 let view = LinearLayout::horizontal()
243 .child(FloatEditView::new().allow_empty(true).full_width())
244 .child(TextView::new(" GB"));
245
246 Self {
247 view,
248 allow_empty: true,
249 }
250 }
251
252 pub fn content(mut self, content: f64) -> Self {
253 if let Some(view) = self.view.get_child_mut(0).and_then(|v| v.downcast_mut()) {
254 *view = FloatEditView::new().content(content).full_width();
255 }
256
257 self
258 }
259
260 pub fn content_maybe(self, content: Option<f64>) -> Self {
261 if let Some(value) = content {
262 self.content(value)
263 } else {
264 self
265 }
266 }
267
268 pub fn max_value(mut self, max: f64) -> Self {
269 if let Some(view) = self
270 .view
271 .get_child_mut(0)
272 .and_then(|v| v.downcast_mut::<ResizedView<FloatEditView>>())
273 {
274 view.get_inner_mut().set_max_value(max);
275 }
276
277 self
278 }
279
280 pub fn get_content(&self) -> Option<f64> {
281 self.with_view(|v| {
282 v.get_child(0)?
283 .downcast_ref::<ResizedView<FloatEditView>>()?
284 .with_view(|v| {
285 if self.allow_empty {
286 v.get_content_maybe().and_then(Result::ok)
287 } else {
288 v.get_content().ok()
289 }
290 })
291 .flatten()
292 })
293 .flatten()
294 }
295 }
296
297 impl ViewWrapper for DiskSizeEditView {
298 cursive::wrap_impl!(self.view: LinearLayout);
299 }
300
301 pub trait FormViewGetValue<R> {
302 fn get_value(&self) -> Option<R>;
303 }
304
305 impl FormViewGetValue<String> for EditView {
306 fn get_value(&self) -> Option<String> {
307 Some((*self.get_content()).clone())
308 }
309 }
310
311 impl<T: 'static + Clone> FormViewGetValue<T> for SelectView<T> {
312 fn get_value(&self) -> Option<T> {
313 self.selection().map(|v| (*v).clone())
314 }
315 }
316
317 impl<T> FormViewGetValue<T> for NumericEditView<T>
318 where
319 T: Copy + ToString + FromStr + PartialOrd,
320 NumericEditView<T>: ViewWrapper,
321 {
322 fn get_value(&self) -> Option<T> {
323 self.get_content().ok()
324 }
325 }
326
327 impl FormViewGetValue<CidrAddress> for CidrAddressEditView {
328 fn get_value(&self) -> Option<CidrAddress> {
329 self.get_values()
330 }
331 }
332
333 impl<T, R> FormViewGetValue<R> for NamedView<T>
334 where
335 T: 'static + FormViewGetValue<R>,
336 NamedView<T>: ViewWrapper,
337 <NamedView<T> as ViewWrapper>::V: FormViewGetValue<R>,
338 {
339 fn get_value(&self) -> Option<R> {
340 self.with_view(|v| v.get_value()).flatten()
341 }
342 }
343
344 impl FormViewGetValue<f64> for DiskSizeEditView {
345 fn get_value(&self) -> Option<f64> {
346 self.get_content()
347 }
348 }
349
350 pub struct FormView {
351 view: LinearLayout,
352 }
353
354 impl FormView {
355 pub fn new() -> Self {
356 let view = LinearLayout::horizontal()
357 .child(LinearLayout::vertical().full_width())
358 .child(LinearLayout::vertical().full_width());
359
360 Self { view }
361 }
362
363 pub fn add_child(&mut self, label: &str, view: impl View) {
364 self.add_to_column(0, TextView::new(format!("{label}: ")).no_wrap());
365 self.add_to_column(1, view);
366 }
367
368 pub fn child(mut self, label: &str, view: impl View) -> Self {
369 self.add_child(label, view);
370 self
371 }
372
373 pub fn child_conditional(mut self, condition: bool, label: &str, view: impl View) -> Self {
374 if condition {
375 self.add_child(label, view);
376 }
377 self
378 }
379
380 pub fn get_child<T: View>(&self, index: usize) -> Option<&T> {
381 self.view
382 .get_child(1)?
383 .downcast_ref::<ResizedView<LinearLayout>>()?
384 .get_inner()
385 .get_child(index)?
386 .downcast_ref::<T>()
387 }
388
389 pub fn get_child_mut<T: View>(&mut self, index: usize) -> Option<&mut T> {
390 self.view
391 .get_child_mut(1)?
392 .downcast_mut::<ResizedView<LinearLayout>>()?
393 .get_inner_mut()
394 .get_child_mut(index)?
395 .downcast_mut::<T>()
396 }
397
398 pub fn get_value<T, R>(&self, index: usize) -> Option<R>
399 where
400 T: View + FormViewGetValue<R>,
401 {
402 self.get_child::<T>(index)?.get_value()
403 }
404
405 pub fn replace_child(&mut self, index: usize, view: impl View) {
406 let parent = self
407 .view
408 .get_child_mut(1)
409 .and_then(|v| v.downcast_mut())
410 .map(ResizedView::<LinearLayout>::get_inner_mut);
411
412 if let Some(parent) = parent {
413 parent.remove_child(index);
414 parent.insert_child(index, view);
415 }
416 }
417
418 pub fn call_on_childs<T: View>(&mut self, callback: &dyn Fn(&mut T)) {
419 for i in 0..self.len() {
420 if let Some(v) = self.get_child_mut::<T>(i) {
421 callback(v);
422 }
423 }
424 }
425
426 pub fn len(&self) -> usize {
427 self.view
428 .get_child(1)
429 .and_then(|v| v.downcast_ref::<ResizedView<LinearLayout>>())
430 .unwrap()
431 .get_inner()
432 .len()
433 }
434
435 fn add_to_column(&mut self, index: usize, view: impl View) {
436 self.view
437 .get_child_mut(index)
438 .and_then(|v| v.downcast_mut::<ResizedView<LinearLayout>>())
439 .unwrap()
440 .get_inner_mut()
441 .add_child(view);
442 }
443 }
444
445 impl ViewWrapper for FormView {
446 cursive::wrap_impl!(self.view: LinearLayout);
447
448 fn wrap_important_area(&self, size: Vec2) -> Rect {
449 // This fixes scrolling on small screen when many elements are present, e.g. bootdisk/RAID
450 // list. Without this, scrolling completely down and then back up would not properly
451 // display the currently selected form element.
452 // tl;dr: For whatever reason, the inner `LinearLayout` calculates the rect with a line
453 // height of 2. So e.g. if the first form element is selected, the y-coordinate is 2, if
454 // the second is selected it is 4 and so on. Knowing that, this can fortunately be quite
455 // easy fixed by just dividing the y-coordinate by 2 and adjusting the size of the area
456 // rectanglo to 1.
457
458 let inner = self.view.important_area(size);
459 let top_left = inner.top_left().map_y(|y| y / 2);
460
461 Rect::from_size(top_left, (inner.width(), 1))
462 }
463 }
464
465 pub struct CidrAddressEditView {
466 view: LinearLayout,
467 }
468
469 impl CidrAddressEditView {
470 pub fn new() -> Self {
471 let view = LinearLayout::horizontal()
472 .child(EditView::new().full_width())
473 .child(TextView::new(" / "))
474 .child(Self::mask_edit_view(0));
475
476 Self { view }
477 }
478
479 pub fn content(mut self, cidr: CidrAddress) -> Self {
480 if let Some(view) = self
481 .view
482 .get_child_mut(0)
483 .and_then(|v| v.downcast_mut::<ResizedView<EditView>>())
484 {
485 *view = EditView::new()
486 .content(cidr.addr().to_string())
487 .full_width();
488 }
489
490 if let Some(view) = self
491 .view
492 .get_child_mut(2)
493 .and_then(|v| v.downcast_mut::<ResizedView<IntegerEditView>>())
494 {
495 *view = Self::mask_edit_view(cidr.mask());
496 }
497
498 self
499 }
500
501 fn mask_edit_view(content: usize) -> ResizedView<IntegerEditView> {
502 IntegerEditView::new()
503 .max_value(128)
504 .max_content_width(3)
505 .content(content)
506 .fixed_width(4)
507 }
508
509 fn get_values(&self) -> Option<CidrAddress> {
510 let addr = self
511 .view
512 .get_child(0)?
513 .downcast_ref::<ResizedView<EditView>>()?
514 .get_inner()
515 .get_content()
516 .parse::<IpAddr>()
517 .ok()?;
518
519 let mask = self
520 .view
521 .get_child(2)?
522 .downcast_ref::<ResizedView<IntegerEditView>>()?
523 .get_inner()
524 .get_content()
525 .ok()?;
526
527 CidrAddress::new(addr, mask).ok()
528 }
529 }
530
531 impl ViewWrapper for CidrAddressEditView {
532 cursive::wrap_impl!(self.view: LinearLayout);
533 }