qcode/ui/profiles/mod.rs
1//! The profiles screen: the profiles a store holds, and the wizard that makes one.
2//!
3//! A profile is what a workspace opens a harness with, so this screen answers two questions about
4//! every profile — is its image built, and is it signed in — and never answers either of them
5//! from memory. Both come from the engine, and until the engine has answered the screen says
6//! that it does not know yet.
7//!
8//! The screen speaks its own [`Msg`], which the application maps to its own message on the way
9//! out of [`update`] and [`view`]. Everything that touches the disk or
10//! the engine happens in a [`Command`], so the state changes by message alone and every state
11//! the screen has — empty, loading, broken, without an engine, mid-build, mid-login — is reached
12//! in a test with no container runtime.
13
14mod actions;
15mod builds;
16#[cfg(test)]
17mod cleanup_live;
18#[cfg(test)]
19mod inbox_tests;
20mod list;
21#[cfg(test)]
22mod live;
23// Public for the README's recording (tests/readme_gif.rs), which builds a profile's image with
24// QCode's own recipe; it is not part of the library's interface, so it is left out of its documentation.
25#[doc(hidden)]
26pub mod recipe;
27mod remove;
28pub(crate) mod shell;
29mod shell_page;
30#[cfg(test)]
31mod shell_tests;
32mod status;
33#[cfg(test)]
34mod tests;
35#[cfg(test)]
36mod window_tests;
37mod wizard;
38mod wizard_view;
39pub(crate) mod work;
40
41use std::collections::HashMap;
42use std::path::PathBuf;
43use std::sync::Arc;
44
45use qframe::diagnostics::Diagnostic;
46use qframe::prelude::*;
47use qframe::widgets::{LogLevel, LogLine, TerminalEvent, TerminalSession, Toast};
48
49use crate::base::Os;
50use crate::desktop::login::{Seen, SignIn};
51use crate::desktop::{Display, NoDisplay, callback};
52use crate::engine::Engine;
53use crate::profile::own::Own;
54use crate::profile::{AccountKind, Extra, MountAccess, Profile, SafeName, Template};
55use crate::provider::{ProviderEntry, Providers};
56use crate::store::Store;
57use crate::ui::stalling;
58
59pub use list::{closed_sign_in, entry, hints, view};
60pub use shell_page::{ShellMsg, ShellPage, Side as ShellSide, Stage as ShellStage};
61pub use status::{Readiness, Revision, Row, Status};
62pub use wizard::{Blocked, Build, Draft, Login, Page, PickRow, Stage, Unfinished, WindowBack, WindowLogin};
63pub use work::Problem;
64
65use actions::{
66 arrive, ask_delete, ask_rebuild, ask_sign_out, cancel_login, current_window, delete, delete_question, deleted,
67 discard, failure_toast, leave, load_providers, login_event, next, probe, reload, report, sign_out, start_build,
68 start_login, store_login, window_asked, window_opened,
69};
70
71/// Width of the wizard's pages: wide enough for every step to keep its name in the row of
72/// steps, and still one column of text. A narrower terminal shrinks it, and the steps fall back
73/// to their markers.
74///
75/// Measured against the seven English steps, whose row is 104 cells, with the frame around it;
76/// German, French, Turkish, Russian, Portuguese and Chinese fit too. Spanish and Japanese name
77/// their steps longer and show the markers with the current step's name, as a narrow terminal
78/// does.
79const PAGE_WIDTH: u16 = 110;
80
81/// Width of the list of profiles: a page of its own, since it is a table of five columns of words
82/// and the words do not fit the page the settings screen keeps. The longest row the list can make
83/// — a profile with a long name, the longest harness name, "signed in inside its window" for its
84/// account and both the longest answers of the engine — is 98 cells, so nothing in the last column
85/// is cut.
86const LIST_WIDTH: u16 = 100;
87
88/// Rows the build log and the login terminal are given, so the buttons under them stay put.
89const VIEWPORT_ROWS: u16 = 16;
90
91/// Rows the tallest page of the wizard takes at [`PAGE_WIDTH`], with the steps above it and the
92/// buttons under it: the image page of a machine without an engine. The wizard is placed as if
93/// every page were this tall, so the steps stay on the same row from page to page.
94const WIZARD_ROWS: u16 = 28;
95
96/// Rows the application's own foot takes under this screen — the way back and the list of keys —
97/// which the screen's view cannot see but which shares the terminal with it.
98const CHROME_ROWS: u16 = 1;
99
100/// What was read out of a store's `Profiles/` folder.
101#[derive(Debug, Clone, PartialEq)]
102pub struct Listing {
103 /// The profiles whose files could be read.
104 pub profiles: Vec<Profile>,
105 /// Everything that was wrong with the files, each with its place in one.
106 pub diagnostics: Vec<Diagnostic>,
107}
108
109/// Everything that can happen on the profiles screen.
110#[derive(Debug, Clone)]
111pub enum Msg {
112 /// Read the store again. This is also how the screen is started.
113 Reload,
114 /// The store was read.
115 Loaded(Listing),
116 /// The providers file was read, on the same reload.
117 ProvidersLoaded(Vec<ProviderEntry>),
118 /// Read the providers file again, and only that: the Providers page was open over this
119 /// screen, and what was added there is what the wizard's account page now offers.
120 ReloadProviders,
121 /// The engine answered about every profile.
122 Probed(Vec<Status>),
123 /// The selection moved to the profile at this place in the list.
124 Select(usize),
125 /// The selection moved to the row that makes a new profile.
126 SelectNew,
127 /// Signing the chosen profile in was asked for, which is how a login that was interrupted is
128 /// picked up again.
129 SignInAsked,
130 /// Signing a profile out was asked for.
131 SignOutAsked,
132 /// The question was answered with yes.
133 SignOutConfirmed,
134 /// The login was removed, or could not be.
135 SignedOut(Result<(), Problem>),
136 /// Building the chosen profile's image again was asked for.
137 RebuildAsked,
138 /// The question was answered with yes: the image page opens on its own and the build starts.
139 RebuildConfirmed,
140 /// Changing the chosen profile was asked for: the wizard opens on it.
141 EditAsked,
142 /// Open the wizard.
143 New,
144 /// Open the wizard as soon as the store has been read, for a person who asked for a new
145 /// profile from somewhere else and should not have to ask a second time here. It waits for
146 /// the listing because the wizard's suggested name steps around the names already taken.
147 NewWhenRead,
148 /// Leave the wizard without keeping anything that has not been made yet.
149 Cancel,
150 /// Go back a page.
151 Back,
152 /// Go on a page.
153 Next,
154 /// Leave the wizard, keeping what was made.
155 Finish,
156 /// Go back to a finished page.
157 Step(usize),
158 /// The name was typed.
159 Name(String),
160 /// A harness was chosen.
161 PickHarness(usize),
162 /// An operating system was chosen, by its place in [`Os::ALL`].
163 PickOs(usize),
164 /// A template was chosen.
165 PickTemplate(usize),
166 /// A part of a QCode template's additions was switched on (`true`) or off.
167 SwitchExtra(Extra, bool),
168 /// An account type was chosen.
169 PickAccount(usize),
170 /// A provider was chosen, on the account page's own list.
171 PickProvider(usize),
172 /// A model of the chosen provider, or one of its lineups, was chosen by its place in the list
173 /// the filter leaves.
174 PickProviderModel(usize),
175 /// The filter of the model list was typed.
176 ModelFilter(String),
177 /// The person asked to go to the Providers page, because the harness they chose can be
178 /// pointed at one but they have not added any yet.
179 ManageProviders,
180 /// An access for `Assets/` was chosen.
181 PickAssets(usize),
182 /// A network mode was chosen.
183 PickNetwork(usize),
184 /// Build the image.
185 BuildStart,
186 /// The base image was not on the machine, so it is being built before the profile's image.
187 BaseImageStarted,
188 /// The build printed a line.
189 BuildLine(String),
190 /// The build was looked at, which is what says whether its log has gone quiet for long enough
191 /// to be stuck.
192 BuildLooked(std::time::Instant),
193 /// The build ended.
194 BuildEnded(Result<(), Problem>),
195 /// Stop the build.
196 BuildCancel,
197 /// Open the container the login happens in.
198 LoginStart,
199 /// The container is up and the harness is running on a terminal.
200 LoginOpened {
201 /// The terminal the harness draws on.
202 session: TerminalSession,
203 /// The container it runs in.
204 container: Arc<work::LoginContainer>,
205 },
206 /// Something happened on the login terminal.
207 LoginEvent(TerminalEvent),
208 /// The person says they have signed in; look for the login and store it.
209 LoginDone,
210 /// Stop the login and leave nothing behind.
211 LoginCancel,
212 /// The login was stored, or it was not.
213 LoginStored(Result<usize, Problem>),
214 /// The container could not be opened.
215 LoginFailed(Problem),
216 /// A container a login used has been cleared away.
217 LoginDiscarded,
218 /// The window a login is made in is open on the person's screen; the number tells this opening
219 /// apart from an earlier one.
220 WindowOpened(u64, Arc<SignIn>),
221 /// The look for the window's login found it, or found the window gone.
222 WindowSeen(u64, Seen),
223 /// The window asked for these web addresses to be opened, the newest last.
224 WindowAsked(u64, Vec<String>),
225 /// The page the window asked for was shown, or could not be.
226 WindowPage(u64, String, Page),
227 /// The listening for a window sign-in's way back ended; the second number is which listening.
228 WindowBack(u64, u64, callback::Ending),
229 /// Deleting the chosen profile was asked for: the engine is asked what it holds of it first.
230 DeleteAsked,
231 /// The engine answered what it holds of the profile to be deleted.
232 DeleteSurveyed(Box<remove::Survey>),
233 /// The question was answered with yes; `true` when the workspaces' homes of it go too.
234 DeleteConfirmed(bool),
235 /// The question was answered with no.
236 DeleteKept,
237 /// The deletion is over.
238 Deleted(Box<remove::Survey>, remove::Outcome),
239 /// Something about a profile's shell, or its own steps.
240 Shell(ShellMsg),
241}
242
243/// The profiles screen.
244#[derive(Debug)]
245pub struct Profiles {
246 root: Option<PathBuf>,
247 engine: Option<Engine>,
248 loading: bool,
249 rows: Vec<Row>,
250 problems: Vec<Diagnostic>,
251 selected: usize,
252 draft: Option<Draft>,
253 signing_out: bool,
254 made: Option<SafeName>,
255 /// The profile the wizard just finished, to be chosen in the list once the list has been read
256 /// again with it in. Kept apart from `made`, which the application takes at once.
257 choose_when_read: Option<SafeName>,
258 /// The providers the person has added, as of the last reload, offered on the wizard's
259 /// account page to a harness that can be pointed at one.
260 providers: Vec<ProviderEntry>,
261 /// Where they are read from: the file the Providers page writes, or nowhere on a machine
262 /// without a data folder.
263 providers_file: Option<PathBuf>,
264 /// The wizard was asked for while the store was still being read; it opens with the listing.
265 new_wanted: bool,
266 /// A profile is being looked at or deleted, so the button takes no second press.
267 deleting: bool,
268 /// What the question being asked is about.
269 doomed: Option<Box<remove::Survey>>,
270 /// Whether the selection is on the row that makes a new profile rather than on a profile.
271 on_new: bool,
272 /// What this machine offers a window, for the sign-in of a harness that opens one.
273 display: Result<Display, NoDisplay>,
274 /// A profile's shell, while it is open; it covers the list or the wizard it was opened from.
275 shell: Option<ShellPage>,
276 /// What each profile had added in its shell, by the profile's name, as last read.
277 own: HashMap<String, Own>,
278 /// The lengths a build's silence is judged by. The product's are the measured ones; a test
279 /// shortens them, since a test cannot wait five minutes for a warning.
280 stall: stalling::Lengths,
281 /// Whether the next look at a build is timed, so that one is timed at a time.
282 watching: bool,
283}
284
285impl Profiles {
286 /// A profiles screen for the store at `root`, using `engine`.
287 ///
288 /// Both are optional because both can be missing on a working machine: a store that has
289 /// not been chosen yet, and an engine that was uninstalled after the setup. The screen opens
290 /// either way and says what it cannot do.
291 #[must_use]
292 pub fn new(root: Option<PathBuf>, engine: Option<Engine>) -> Self {
293 Self {
294 loading: root.is_some(),
295 root,
296 engine,
297 rows: Vec::new(),
298 problems: Vec::new(),
299 selected: 0,
300 draft: None,
301 signing_out: false,
302 made: None,
303 choose_when_read: None,
304 providers: Vec::new(),
305 providers_file: Providers::file(),
306 new_wanted: false,
307 deleting: false,
308 doomed: None,
309 on_new: false,
310 display: crate::desktop::current(),
311 shell: None,
312 own: HashMap::new(),
313 stall: stalling::Lengths::default(),
314 watching: false,
315 }
316 }
317
318 /// The same screen, opening a sign-in window on `display` rather than on the session this
319 /// process was started in: a test's own stand-in, never the person's screen.
320 #[must_use]
321 pub fn showing_on(mut self, display: Result<Display, NoDisplay>) -> Self {
322 self.display = display;
323 self
324 }
325
326 /// The same screen, saying a build is stuck after `lengths.quiet` of silence and looking every
327 /// `lengths.look`, which the product uses as [`stalling::QUIET`] and [`stalling::LOOK`].
328 ///
329 /// Only for a test: the product uses those two measured lengths, since a test cannot wait five
330 /// minutes to hear a warning and cannot ask the engine for a build that really takes that long.
331 #[cfg(test)]
332 #[must_use]
333 pub fn stalling(mut self, lengths: stalling::Lengths) -> Self {
334 self.stall = lengths;
335 self
336 }
337
338 /// The same screen reading the providers from `path` rather than from this machine's data
339 /// folder, so that it offers exactly what the Providers page it sends the person to wrote.
340 #[must_use]
341 pub fn with_providers_file(mut self, path: Option<PathBuf>) -> Self {
342 self.providers_file = path;
343 self
344 }
345
346 /// The profile the wizard has just finished making, taken once.
347 ///
348 /// It is how this screen tells the application that the work it was opened for is done. Who
349 /// opened it decides what that means: a workspace waiting behind this screen for a profile it
350 /// can open a tab with gets it back, and nobody else is moved.
351 pub fn just_made(&mut self) -> Option<SafeName> {
352 self.made.take()
353 }
354
355 /// The profiles and what the engine said about them.
356 #[must_use]
357 pub fn rows(&self) -> &[Row] {
358 &self.rows
359 }
360
361 /// The profile the selection is on.
362 #[must_use]
363 pub fn selected(&self) -> Option<&Row> {
364 self.rows.get(self.selected)
365 }
366
367 /// The wizard, while it is open.
368 #[must_use]
369 pub fn draft(&self) -> Option<&Draft> {
370 self.draft.as_ref()
371 }
372
373 /// Whether the store is still being read.
374 #[must_use]
375 pub fn is_loading(&self) -> bool {
376 self.loading
377 }
378
379 /// Everything that was wrong with the definition files.
380 #[must_use]
381 pub fn problems(&self) -> &[Diagnostic] {
382 &self.problems
383 }
384
385 /// A profile's shell, while it is open.
386 #[must_use]
387 pub fn shell(&self) -> Option<&ShellPage> {
388 self.shell.as_ref()
389 }
390
391 /// The store, when there is one.
392 fn store(&self) -> Option<Store> {
393 self.root.as_ref().map(Store::new)
394 }
395}
396
397/// Applies a message to the screen. Everything the returned [`Command`] later delivers comes
398/// back through the same `update`.
399pub fn update(state: &mut Profiles, message: Msg) -> Command<Msg> {
400 match message {
401 Msg::Reload => reload(state),
402 Msg::Loaded(listing) => {
403 state.loading = false;
404 state.problems = listing.diagnostics;
405 state.rows = listing.profiles.iter().cloned().map(Row::new).collect();
406 state.selected = state.selected.min(state.rows.len().saturating_sub(1));
407 // A person who just made a profile is looking for it, not for whatever stood first.
408 // A reading that was already under way when the wizard closed does not have it yet, so
409 // the wish waits for the reading that does.
410 let made = state.choose_when_read.as_ref();
411 if let Some(index) = made.and_then(|made| state.rows.iter().position(|row| row.profile.name == *made)) {
412 state.selected = index;
413 state.choose_when_read = None;
414 }
415 state.own.clear();
416 let own = shell_page::load(state.store(), &listing.profiles);
417 let probing = Command::batch([probe(state, listing.profiles), own]);
418 if std::mem::take(&mut state.new_wanted) {
419 return Command::batch([probing, update(state, Msg::New)]);
420 }
421 probing
422 }
423 Msg::ProvidersLoaded(providers) => {
424 // A wizard that is open offers what the file has now, not what it had when it opened.
425 if let Some(draft) = &mut state.draft {
426 draft.offer_providers(providers.clone());
427 }
428 state.providers = providers;
429 Command::none()
430 }
431 Msg::ReloadProviders => load_providers(state),
432 Msg::Probed(answers) => {
433 for row in &mut state.rows {
434 for answer in &answers {
435 row.apply(answer);
436 }
437 }
438 Command::none()
439 }
440 Msg::Select(index) => {
441 if index < state.rows.len() {
442 state.selected = index;
443 state.on_new = false;
444 }
445 Command::none()
446 }
447 Msg::SelectNew => {
448 state.on_new = true;
449 Command::none()
450 }
451 Msg::SignInAsked => {
452 // A profile without an image has nothing to sign in from; the list says so already.
453 let Some(row) = state.selected() else { return Command::none() };
454 if state.engine.is_none() || row.image != Readiness::Present || !row.profile.account.needs_login() {
455 return Command::none();
456 }
457 state.draft = Some(Draft::for_login(&row.profile));
458 Command::focus("login-open")
459 }
460 Msg::SignOutAsked => ask_sign_out(state),
461 Msg::RebuildAsked => ask_rebuild(state),
462 Msg::RebuildConfirmed => {
463 let Some(row) = state.selected() else { return Command::none() };
464 if state.engine.is_none() || row.image != Readiness::Present {
465 return Command::none();
466 }
467 state.draft = Some(Draft::for_rebuild(&row.profile));
468 start_build(state)
469 }
470 Msg::EditAsked => {
471 let Some(row) = state.selected() else { return Command::none() };
472 state.draft = Some(Draft::for_edit(&row.profile, state.providers.clone()));
473 Command::focus("profile-harness")
474 }
475 Msg::SignOutConfirmed => sign_out(state),
476 Msg::SignedOut(result) => {
477 state.signing_out = false;
478 match result {
479 Ok(()) => Command::batch([Command::toast(Toast::success(t!("profiles.signed-out"))), reload(state)]),
480 Err(problem) => Command::toast(failure_toast("profiles.sign-out-failed", &problem)),
481 }
482 }
483 Msg::NewWhenRead if state.loading => {
484 state.new_wanted = true;
485 Command::none()
486 }
487 Msg::NewWhenRead | Msg::New => {
488 state.choose_when_read = None;
489 let taken = state.rows.iter().map(|row| row.profile.name.as_str().to_owned());
490 state.draft = Some(Draft::new(taken, state.providers.clone()));
491 // The page's first question, the harness, above the name that follows from it.
492 Command::focus("profile-harness")
493 }
494 Msg::Cancel => leave(state),
495 // Finish stands where Next would on the last page. A profile with no sign-in ends on its
496 // image page, where finishing before the image is built would leave with nothing made,
497 // so it is answered the way Next is: with the way to the image.
498 Msg::Finish => {
499 if state.draft.as_ref().is_some_and(|draft| draft.blocked().is_some()) {
500 return next(state);
501 }
502 // Finish is only offered once there is a profile; Cancel is the other way out and
503 // leaves nothing behind, so only this one is a profile having been made. A profile
504 // that was only changed was not made: nobody waiting for a new one is handed it.
505 let finished = state.draft.as_ref().and_then(Draft::profile).map(|profile| profile.name);
506 if state.draft.as_ref().is_some_and(Draft::is_editing) {
507 state.choose_when_read = finished;
508 } else {
509 state.made = finished;
510 state.choose_when_read.clone_from(&state.made);
511 }
512 leave(state)
513 }
514 Msg::Back => {
515 let Some(draft) = &mut state.draft else { return Command::none() };
516 draft.back();
517 arrive(draft)
518 }
519 Msg::Step(index) => {
520 let Some(draft) = &mut state.draft else { return Command::none() };
521 draft.go_to(index);
522 arrive(draft)
523 }
524 Msg::Next => next(state),
525 Msg::Name(name) => {
526 if let Some(draft) = &mut state.draft {
527 draft.renamed = true;
528 draft.name = name;
529 }
530 Command::none()
531 }
532 Msg::PickHarness(index) => {
533 if let Some(draft) = &mut state.draft
534 && let Some(harness) = draft.harnesses().get(index).copied()
535 {
536 draft.choose_harness(harness);
537 }
538 Command::none()
539 }
540 Msg::PickOs(index) => {
541 if let (Some(draft), Some(os)) = (&mut state.draft, Os::ALL.get(index)) {
542 draft.choose_os(*os);
543 }
544 Command::none()
545 }
546 Msg::PickTemplate(index) => {
547 if let Some(draft) = &mut state.draft
548 && let Some(template) = Template::offered(draft.harness).get(index).copied()
549 {
550 draft.choose(template);
551 }
552 Command::none()
553 }
554 Msg::SwitchExtra(extra, on) => {
555 if let Some(draft) = &mut state.draft {
556 draft.switch(extra, on);
557 }
558 Command::none()
559 }
560 Msg::PickAccount(index) => {
561 if let Some(draft) = &mut state.draft
562 && let Some(account) = draft.harness.record().accounts.get(index)
563 {
564 draft.account = *account;
565 // Picking a provider is worth nothing on its own; the person still has to name a
566 // model, so the first of whichever provider stands first is chosen along with it,
567 // the same courtesy `choose_harness` pays the account page itself.
568 let first_tag = draft.providers.first().map(|entry| entry.tag.to_string());
569 if *account == AccountKind::Provider
570 && let Some(tag) = first_tag
571 {
572 draft.choose_provider(&tag);
573 }
574 }
575 Command::none()
576 }
577 Msg::PickProvider(index) => {
578 if let Some(draft) = &mut state.draft {
579 let tag = draft.providers.get(index).map(|entry| entry.tag.to_string());
580 if let Some(tag) = tag {
581 draft.choose_provider(&tag);
582 }
583 }
584 Command::none()
585 }
586 Msg::PickProviderModel(index) => {
587 if let Some(draft) = &mut state.draft {
588 draft.choose_pick_row(index);
589 }
590 Command::none()
591 }
592 Msg::ModelFilter(query) => {
593 if let Some(draft) = &mut state.draft {
594 draft.model_filter = query;
595 }
596 Command::none()
597 }
598 Msg::ManageProviders => Command::none(),
599 Msg::PickAssets(index) => {
600 if let (Some(draft), Some(access)) = (&mut state.draft, MountAccess::ALL.get(index)) {
601 draft.assets = *access;
602 }
603 Command::none()
604 }
605 Msg::PickNetwork(index) => {
606 if let Some(draft) = &mut state.draft
607 && let Some(mode) = draft.networks().get(index).copied()
608 {
609 draft.network = mode;
610 }
611 Command::none()
612 }
613 Msg::BuildStart => start_build(state),
614 Msg::BaseImageStarted => {
615 if let Some(draft) = &mut state.draft {
616 draft.log.push(LogLine::new(LogLevel::Info, t!("profiles.wizard.build-base")));
617 // A line of the build's own is what ends the silence the page warns about.
618 draft.build_said();
619 }
620 Command::none()
621 }
622 Msg::BuildLine(text) => {
623 if let Some(draft) = &mut state.draft {
624 draft.log.push(LogLine::new(LogLevel::Info, text));
625 draft.build_said();
626 }
627 Command::none()
628 }
629 // The look came round: the draft is marked by how long its log has been quiet, and the
630 // next look is timed while a build is still running.
631 Msg::BuildLooked(now) => builds::looked(state, now),
632 Msg::BuildEnded(result) => {
633 let Some(draft) = &mut state.draft else { return Command::none() };
634 match result {
635 Ok(()) => draft.build = Build::Done,
636 Err(problem) => {
637 report(&mut draft.log, &problem);
638 draft.build = if problem == Problem::Cancelled { Build::Stopped } else { Build::Failed(problem) };
639 }
640 }
641 draft.build_ended();
642 Command::none()
643 }
644 Msg::BuildCancel => {
645 let Some(draft) = &mut state.draft else { return Command::none() };
646 let Build::Running(id) = draft.build else { return Command::none() };
647 // The build removes the half-made image itself as soon as it notices the stop.
648 draft.build = Build::Stopped;
649 draft.build_ended();
650 let stopping =
651 if draft.is_only_rebuild() { "profiles.rebuild.stopping" } else { "profiles.wizard.build-stopping" };
652 draft.log.push(LogLine::new(LogLevel::Warn, t!(stopping)));
653 Command::cancel_task(id)
654 }
655 Msg::LoginStart => start_login(state),
656 Msg::LoginOpened { session, container } => {
657 let engine = state.engine.clone();
658 let Some(draft) = &mut state.draft else {
659 // The wizard was closed while the container was coming up; nothing keeps it.
660 session.kill();
661 return discard(engine, container);
662 };
663 let watch = session.watch();
664 draft.login = Login::Running { session, container };
665 Command::batch([Command::perform(move || Msg::LoginEvent(watch.next())), Command::focus("login-terminal")])
666 }
667 Msg::LoginEvent(event) => login_event(state, event),
668 Msg::LoginDone => store_login(state),
669 Msg::LoginCancel => cancel_login(state),
670 Msg::LoginStored(result) => {
671 let Some(draft) = &mut state.draft else { return Command::none() };
672 draft.login = match result {
673 Ok(stored) => Login::Stored(stored),
674 Err(Problem::NoLogin) => Login::Unfinished(Unfinished::Missing),
675 Err(problem) => Login::Failed(problem),
676 };
677 Command::none()
678 }
679 Msg::LoginFailed(problem) => {
680 if let Some(draft) = &mut state.draft {
681 draft.login = Login::Failed(problem);
682 }
683 Command::none()
684 }
685 Msg::LoginDiscarded => Command::none(),
686 Msg::WindowOpened(run, sign_in) => window_opened(state, run, sign_in),
687 Msg::WindowSeen(run, seen) => {
688 let current = state
689 .draft
690 .as_ref()
691 .is_some_and(|draft| matches!(&draft.login, Login::Window(window) if window.run == run));
692 // A window that is signed in, or gone, is done with: what its home holds is taken now.
693 if current && seen != Seen::NotYet { store_login(state) } else { Command::none() }
694 }
695 Msg::WindowAsked(run, addresses) => window_asked(state, run, &addresses),
696 Msg::WindowPage(run, address, page) => {
697 if let Some(window) = current_window(state, run) {
698 window.page = Some((address, page));
699 }
700 Command::none()
701 }
702 Msg::WindowBack(run, id, ending) => {
703 if let Some(window) = current_window(state, run) {
704 window.came_back(id, ending);
705 }
706 Command::none()
707 }
708 Msg::DeleteAsked => ask_delete(state),
709 Msg::DeleteSurveyed(survey) => delete_question(state, survey),
710 Msg::DeleteConfirmed(homes) => delete(state, homes),
711 Msg::DeleteKept => {
712 state.deleting = false;
713 state.doomed = None;
714 Command::none()
715 }
716 Msg::Deleted(survey, outcome) => deleted(state, &survey, outcome),
717 Msg::Shell(message) => shell_page::update(state, message),
718 }
719}