]> git.proxmox.com Git - pve-installer.git/blame - proxmox-tui-installer/src/main.rs
tui: rewrite password using new `FormView`
[pve-installer.git] / proxmox-tui-installer / src / main.rs
CommitLineData
183e2a76
CH
1#![forbid(unsafe_code)]
2
b7828c87 3mod options;
fc187196 4mod utils;
83071f35
CH
5mod views;
6
b7828c87 7use crate::options::*;
183e2a76 8use cursive::{
ccf3b075 9 event::Event,
af0dfe0e 10 view::{Nameable, Resizable, ViewWrapper},
183e2a76 11 views::{
947fe360
CH
12 Button, Checkbox, Dialog, DummyView, EditView, LinearLayout, PaddedView, Panel,
13 ResizedView, ScrollView, SelectView, TextView,
183e2a76
CH
14 },
15 Cursive, View,
16};
af0dfe0e 17use views::{
767843f9
CH
18 BootdiskOptionsView, CidrAddressEditView, FormView, FormInputView, FormInputViewGetValue,
19 TableView, TableViewItem,
af0dfe0e 20};
183e2a76
CH
21
22// TextView::center() seems to garble the first two lines, so fix it manually here.
23const LOGO: &str = r#"
24 ____ _ __ _____
25 / __ \_________ _ ______ ___ ____ _ __ | | / / ____/
26 / /_/ / ___/ __ \| |/_/ __ `__ \/ __ \| |/_/ | | / / __/
27 / ____/ / / /_/ /> </ / / / / / /_/ /> < | |/ / /___
28/_/ /_/ \____/_/|_/_/ /_/ /_/\____/_/|_| |___/_____/
29"#;
30
31const TITLE: &str = "Proxmox VE Installer";
32
33struct InstallerView {
8b43c2d3 34 view: ResizedView<LinearLayout>,
183e2a76
CH
35}
36
37impl InstallerView {
f522afb1 38 pub fn new<T: View>(view: T, next_cb: Box<dyn Fn(&mut Cursive)>) -> Self {
8807d547
CH
39 let inner = LinearLayout::vertical()
40 .child(PaddedView::lrtb(0, 0, 1, 1, view))
41 .child(PaddedView::lrtb(
42 1,
43 1,
44 0,
45 0,
46 LinearLayout::horizontal()
47 .child(abort_install_button())
48 .child(DummyView.full_width())
49 .child(Button::new("Previous", switch_to_prev_screen))
50 .child(DummyView)
51 .child(Button::new("Next", next_cb)),
52 ));
f522afb1
CH
53
54 Self::with_raw(inner)
55 }
56
57 pub fn with_raw<T: View>(view: T) -> Self {
8b43c2d3
CH
58 let inner = LinearLayout::vertical()
59 .child(PaddedView::lrtb(1, 1, 0, 1, TextView::new(LOGO).center()))
60 .child(Dialog::around(view).title(TITLE));
61
183e2a76 62 Self {
8b43c2d3
CH
63 // Limit the maximum to something reasonable, such that it won't get spread out much
64 // depending on the screen.
65 view: ResizedView::with_max_size((120, 40), inner),
183e2a76
CH
66 }
67 }
68}
69
70impl ViewWrapper for InstallerView {
8b43c2d3 71 cursive::wrap_impl!(self.view: ResizedView<LinearLayout>);
183e2a76
CH
72}
73
ed191e60
CH
74#[derive(Clone)]
75struct InstallerData {
76 options: InstallerOptions,
77 available_disks: Vec<Disk>,
78}
79
183e2a76
CH
80fn main() {
81 let mut siv = cursive::termion();
82
ccf3b075
CH
83 siv.clear_global_callbacks(Event::CtrlChar('c'));
84 siv.set_on_pre_event(Event::CtrlChar('c'), trigger_abort_install_dialog);
85
ed191e60
CH
86 // TODO: retrieve actual disk info
87 let available_disks = vec![Disk {
e9557e62
CH
88 path: "/dev/vda".to_owned(),
89 size: 17179869184,
90 }];
ed191e60
CH
91
92 siv.set_user_data(InstallerData {
93 options: InstallerOptions {
94 bootdisk: BootdiskOptions::defaults_from(&available_disks[0]),
95 timezone: TimezoneOptions::default(),
96 password: PasswordOptions::default(),
97 network: NetworkOptions::default(),
e9557e62 98 },
ed191e60 99 available_disks,
e9557e62
CH
100 });
101
5e73fcfe 102 add_next_screen(&mut siv, &license_dialog);
183e2a76
CH
103 siv.run();
104}
105
5e73fcfe
CH
106fn add_next_screen(siv: &mut Cursive, constructor: &dyn Fn(&mut Cursive) -> InstallerView) {
107 let v = constructor(siv);
108 siv.add_active_screen();
109 siv.screen_mut().add_layer(v);
183e2a76
CH
110}
111
64220ff1
CH
112fn switch_to_prev_screen(siv: &mut Cursive) {
113 let id = siv.active_screen().saturating_sub(1);
114 siv.set_screen(id);
115}
116
bb951500 117#[cfg(not(debug_assertions))]
183e2a76
CH
118fn yes_no_dialog(
119 siv: &mut Cursive,
120 title: &str,
121 text: &str,
122 callback: &'static dyn Fn(&mut Cursive),
123) {
124 siv.add_layer(
125 Dialog::around(TextView::new(text))
126 .title(title)
127 .dismiss_button("No")
128 .button("Yes", callback),
129 )
130}
131
ccf3b075 132fn trigger_abort_install_dialog(siv: &mut Cursive) {
58869243
CH
133 #[cfg(debug_assertions)]
134 siv.quit();
135
136 #[cfg(not(debug_assertions))]
ccf3b075
CH
137 yes_no_dialog(
138 siv,
139 "Abort installation?",
140 "Are you sure you want to abort the installation?",
141 &Cursive::quit,
142 )
143}
144
183e2a76 145fn abort_install_button() -> Button {
ccf3b075 146 Button::new("Abort", trigger_abort_install_dialog)
183e2a76
CH
147}
148
149fn get_eula() -> String {
8f5fdd21
CH
150 // TODO: properly using info from Proxmox::Install::Env::setup()
151 std::fs::read_to_string("/cdrom/EULA")
152 .unwrap_or_else(|_| "< Debug build - ignoring non-existing EULA >".to_owned())
183e2a76
CH
153}
154
fbccf72c 155fn license_dialog(_: &mut Cursive) -> InstallerView {
183e2a76
CH
156 let inner = LinearLayout::vertical()
157 .child(PaddedView::lrtb(
158 0,
159 0,
160 1,
161 0,
162 TextView::new("END USER LICENSE AGREEMENT (EULA)").center(),
163 ))
8b43c2d3
CH
164 .child(Panel::new(ScrollView::new(
165 TextView::new(get_eula()).center(),
183e2a76
CH
166 )))
167 .child(PaddedView::lrtb(
168 1,
169 1,
f522afb1 170 1,
183e2a76
CH
171 0,
172 LinearLayout::horizontal()
173 .child(abort_install_button())
174 .child(DummyView.full_width())
5e73fcfe
CH
175 .child(Button::new("I agree", |siv| {
176 add_next_screen(siv, &bootdisk_dialog)
177 })),
183e2a76
CH
178 ));
179
f522afb1 180 InstallerView::with_raw(inner)
183e2a76
CH
181}
182
e70f1b2f 183fn bootdisk_dialog(siv: &mut Cursive) -> InstallerView {
ed191e60 184 let data = siv.user_data::<InstallerData>().cloned().unwrap();
64220ff1 185
f522afb1 186 InstallerView::new(
ed191e60
CH
187 BootdiskOptionsView::new(&data.available_disks, &data.options.bootdisk)
188 .with_name("bootdisk-options"),
f522afb1
CH
189 Box::new(|siv| {
190 let options = siv
ed191e60 191 .call_on_name("bootdisk-options", BootdiskOptionsView::get_values)
93ebe7bd 192 .flatten();
f522afb1 193
93ebe7bd 194 if let Some(options) = options {
ed191e60
CH
195 siv.with_user_data(|data: &mut InstallerData| {
196 data.options.bootdisk = options;
93ebe7bd 197 });
f522afb1 198
5e73fcfe 199 add_next_screen(siv, &timezone_dialog);
93ebe7bd
CH
200 } else {
201 siv.add_layer(Dialog::info("Invalid values"));
202 }
f522afb1
CH
203 }),
204 )
64220ff1
CH
205}
206
a142f457
CH
207fn timezone_dialog(siv: &mut Cursive) -> InstallerView {
208 let options = siv
ed191e60
CH
209 .user_data::<InstallerData>()
210 .map(|data| data.options.timezone.clone())
a142f457
CH
211 .unwrap_or_default();
212
767843f9
CH
213 let inner = FormView::new()
214 .child("Country", EditView::new().content("Austria"))
215 .child("Timezone", EditView::new().content(options.timezone))
216 .child(
a142f457 217 "Keyboard layout",
767843f9
CH
218 EditView::new().content(options.kb_layout),
219 )
220 .with_name("timezone-options");
a142f457
CH
221
222 InstallerView::new(
223 inner,
224 Box::new(|siv| {
767843f9
CH
225 let options: Option<Result<TimezoneOptions, String>> =
226 siv.call_on_name("timezone-options", |view: &mut FormView| {
227 let timezone = view
228 .get_value::<EditView, _>(1)
229 .ok_or("failed to retrieve timezone")?;
a142f457 230
767843f9
CH
231 let kb_layout = view
232 .get_value::<EditView, _>(2)
233 .ok_or("failed to retrieve keyboard layout")?;
c2eee468 234
767843f9 235 Ok(TimezoneOptions {
93ebe7bd
CH
236 timezone,
237 kb_layout,
767843f9 238 })
93ebe7bd
CH
239 });
240
767843f9
CH
241 match options {
242 Some(Ok(options)) => {
243 siv.with_user_data(|data: &mut InstallerData| {
244 data.options.timezone = options;
245 });
246
247 add_next_screen(siv, &password_dialog);
248 }
249 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
250 _ => siv.add_layer(Dialog::info("Invalid values")),
93ebe7bd 251 }
a142f457
CH
252 }),
253 )
183e2a76 254}
c2eee468
CH
255
256fn password_dialog(siv: &mut Cursive) -> InstallerView {
257 let options = siv
ed191e60
CH
258 .user_data::<InstallerData>()
259 .map(|data| data.options.password.clone())
c2eee468
CH
260 .unwrap_or_default();
261
15832d18
CH
262 let inner = FormView::new()
263 .child("Root password", EditView::new().secret())
264 .child("Confirm root password", EditView::new().secret())
265 .child("Administator email", EditView::new().content(options.email))
ef4957e8 266 .with_name("password-options");
c2eee468 267
ccd34284
CH
268 InstallerView::new(
269 inner,
270 Box::new(|siv| {
15832d18
CH
271 let options = siv.call_on_name("password-options", |view: &mut FormView| {
272 let root_password = view
273 .get_value::<EditView, _>(0)
274 .ok_or("failed to retrieve password")?;
275
276 let confirm_password = view
277 .get_value::<EditView, _>(1)
278 .ok_or("failed to retrieve password confirmation")?;
ef4957e8 279
15832d18
CH
280 let email = view
281 .get_value::<EditView, _>(2)
282 .ok_or("failed to retrieve email")?;
ef4957e8
CH
283
284 // TODO: proper validation
285 if root_password != confirm_password {
286 Err("passwords do not match")
287 } else {
288 Ok(PasswordOptions {
289 root_password,
15832d18 290 email,
ef4957e8
CH
291 })
292 }
293 });
294
295 match options {
296 Some(Ok(options)) => {
297 siv.with_user_data(|data: &mut InstallerData| {
298 data.options.password = options;
299 });
300
301 add_next_screen(siv, &network_dialog);
302 }
303 Some(Err(err)) => siv.add_layer(Dialog::info(format!("Invalid values: {err}"))),
304 _ => siv.add_layer(Dialog::info("Invalid values")),
305 }
ccd34284
CH
306 }),
307 )
308}
309
310fn network_dialog(siv: &mut Cursive) -> InstallerView {
311 let options = siv
ed191e60
CH
312 .user_data::<InstallerData>()
313 .map(|data| data.options.network.clone())
ccd34284
CH
314 .unwrap_or_default();
315
316 let inner = LinearLayout::vertical()
317 .child(FormInputView::new(
318 "Management interface",
319 SelectView::new().popup().with_all_str(vec!["eth0"]),
320 ))
321 .child(FormInputView::new(
322 "Hostname (FQDN)",
323 EditView::new().content(options.fqdn),
324 ))
325 .child(FormInputView::new(
326 "IP address (CIDR)",
fc187196 327 CidrAddressEditView::new().content(options.address),
ccd34284
CH
328 ))
329 .child(FormInputView::new(
330 "Gateway address",
331 EditView::new().content(options.gateway.to_string()),
332 ))
333 .child(FormInputView::new(
1397feec 334 "DNS server address",
ccd34284 335 EditView::new().content(options.dns_server.to_string()),
cd383717
CH
336 ))
337 .with_name("network-options");
ccd34284 338
947fe360
CH
339 InstallerView::new(
340 inner,
341 Box::new(|siv| {
cd383717
CH
342 let options = siv.call_on_name("network-options", |view: &mut LinearLayout| {
343 fn get_val<T, R>(view: &LinearLayout, index: usize) -> Option<R>
344 where
345 T: View,
346 FormInputView<T>: FormInputViewGetValue<R>,
347 {
348 view.get_child(index)?
349 .downcast_ref::<FormInputView<T>>()?
350 .get_value()
351 }
352
cd383717
CH
353 Some(NetworkOptions {
354 ifname: get_val::<SelectView, _>(view, 0)?,
355 fqdn: get_val::<EditView, _>(view, 1)?,
fc187196 356 address: get_val::<CidrAddressEditView, _>(view, 2)?,
cd383717
CH
357 gateway: get_val::<EditView, _>(view, 3).and_then(|s| s.parse().ok())?,
358 dns_server: get_val::<EditView, _>(view, 3).and_then(|s| s.parse().ok())?,
359 })
360 });
361
362 if let Some(options) = options.flatten() {
ed191e60
CH
363 siv.with_user_data(|data: &mut InstallerData| {
364 data.options.network = options;
cd383717
CH
365 });
366
5e73fcfe 367 add_next_screen(siv, &summary_dialog);
e95f4f1a
CH
368 } else {
369 siv.add_layer(Dialog::info("Invalid values"));
cd383717 370 }
947fe360
CH
371 }),
372 )
373}
374
b7828c87 375pub struct SummaryOption {
947fe360
CH
376 name: &'static str,
377 value: String,
378}
379
380impl SummaryOption {
381 pub fn new<S: Into<String>>(name: &'static str, value: S) -> Self {
382 Self {
383 name,
384 value: value.into(),
385 }
386 }
387}
388
389impl TableViewItem for SummaryOption {
390 fn get_column(&self, name: &str) -> String {
391 match name {
392 "name" => self.name.to_owned(),
393 "value" => self.value.clone(),
394 _ => unreachable!(),
395 }
396 }
397}
398
399fn summary_dialog(siv: &mut Cursive) -> InstallerView {
400 let options = siv
ed191e60
CH
401 .user_data::<InstallerData>()
402 .map(|d| d.options.clone())
947fe360
CH
403 .unwrap();
404
405 let inner = LinearLayout::vertical()
406 .child(PaddedView::lrtb(
407 0,
408 0,
409 1,
410 2,
411 TableView::new()
412 .columns(&[
413 ("name".to_owned(), "Option".to_owned()),
414 ("value".to_owned(), "Selected value".to_owned()),
415 ])
416 .items(options.to_summary()),
417 ))
418 .child(
419 LinearLayout::horizontal()
420 .child(DummyView.full_width())
421 .child(Checkbox::new().with_name("reboot-after-install"))
422 .child(
423 TextView::new(" Automatically reboot after successful installation").no_wrap(),
424 )
425 .child(DummyView.full_width()),
426 )
427 .child(PaddedView::lrtb(
428 1,
429 1,
430 1,
431 0,
432 LinearLayout::horizontal()
433 .child(abort_install_button())
434 .child(DummyView.full_width())
435 .child(Button::new("Previous", switch_to_prev_screen))
436 .child(DummyView)
437 .child(Button::new("Install", |_| {})),
438 ));
439
440 InstallerView::with_raw(inner)
c2eee468 441}