helix-toolbox 1.24.17

Build, configure and optimize a Linux machine from a terminal menu: installs, dotfiles, updates, backups and health checks, every change planned and confirmed first.
//! The Desktop screen.

use super::super::*;

impl App {
    pub(in crate::tui::app) fn desktop_here(&self) -> helix_toolbox_engine::desktop::Desktop {
        if self.examine_here {
            crate::setup::desktop_here()
        } else {
            self.desktop.clone().unwrap_or_default()
        }
    }

    pub(in crate::tui::app) fn selected_desktop_row(&self) -> Option<&DesktopRow> {
        self.desktop_rows.get(self.main_state.selected()?)
    }

    fn looks_directory(&self) -> Option<std::path::PathBuf> {
        self.looks_dir
            .clone()
            .or_else(helix_toolbox_engine::desktop::dir)
    }

    pub(in crate::tui::app) fn reload_desktop(&mut self) {
        let found = self.desktop_here();
        let mut rows = vec![DesktopRow::Heading("This desktop".to_string())];
        let name = match (&found.name, &found.session) {
            (Some(name), Some(session)) => format!("{name}, {session} session"),
            (Some(name), None) => name.clone(),
            (None, _) => "No desktop environment found".to_string(),
        };
        rows.push(DesktopRow::Info {
            details: crate::setup::desktop_text(&found),
            text: name,
        });
        if let Some(manager) = &found.display_manager {
            rows.push(DesktopRow::Info {
                text: format!(
                    "Login screen: {}{}",
                    manager.name,
                    manager
                        .theme
                        .as_deref()
                        .map(|theme| format!(", theme {theme}"))
                        .unwrap_or_default()
                ),
                details: format!(
                    "The display manager {} runs the login screen.\n\nIts configuration:\n{}",
                    manager.name,
                    if manager.files.is_empty() {
                        "  none found".to_string()
                    } else {
                        manager
                            .files
                            .iter()
                            .map(|file| format!("  {file}"))
                            .collect::<Vec<_>>()
                            .join("\n")
                    }
                ),
            });
        }
        if !found.appearance.is_empty() {
            rows.push(DesktopRow::Heading("Appearance".to_string()));
        }
        for value in &found.appearance {
            rows.push(DesktopRow::Info {
                text: format!(
                    "{:<16}{}",
                    value.label,
                    value.value.as_deref().unwrap_or("not set")
                ),
                details: format!(
                    "{}: {}\n\ns saves the whole look, to put back later from this screen.",
                    value.label,
                    value.value.as_deref().unwrap_or("not set")
                ),
            });
        }
        for owner in &found.owners {
            rows.push(DesktopRow::Info {
                text: format!("Note: {owner}"),
                details: owner.clone(),
            });
        }
        if let Some(blocked) = &found.blocked {
            rows.push(DesktopRow::Info {
                text: format!("Not changed here: {blocked}"),
                details: blocked.clone(),
            });
        }
        let looks = self
            .looks_directory()
            .map(|directory| helix_toolbox_engine::desktop::load_all(&directory).0)
            .unwrap_or_default();
        if !looks.is_empty() {
            rows.push(DesktopRow::Heading("Saved looks".to_string()));
        }
        for (slug, look) in looks {
            let other = found.kind.map(|kind| kind.id()) != Some(look.desktop.as_str());
            rows.push(DesktopRow::Look {
                text: format!(
                    "{}{}",
                    helix_toolbox_core::printable(&look.name),
                    if other {
                        format!(
                            "  (saved on {})",
                            helix_toolbox_core::printable(&look.desktop)
                        )
                    } else {
                        format!("  ({} settings)", look.settings.len())
                    }
                ),
                slug,
            });
        }
        let offered = helix_toolbox_engine::setup::load(self.catalog.root())
            .map(|setup| setup.for_desktop(found.kind))
            .unwrap_or_default();
        let offered: Vec<_> =
            helix_toolbox_engine::setup::suggestions(&offered, &self.catalog, &self.platform)
                .into_iter()
                .filter(|suggestion| suggestion.available)
                .collect();
        if !offered.is_empty() {
            rows.push(DesktopRow::Heading("For this desktop".to_string()));
        }
        for suggestion in offered {
            let queued = self.queue.iter().any(|entry| entry.id == suggestion.id);
            rows.push(DesktopRow::Entry {
                text: format!(
                    "{}{}",
                    suggestion.name,
                    if queued { "  · planned" } else { "" }
                ),
                id: suggestion.id,
                note: suggestion.note,
            });
        }
        self.desktop = Some(found);
        self.desktop_rows = rows;
        self.clamp_main();
    }

    pub(in crate::tui::app) fn save_look_as(&mut self, name: &str) {
        let found = self.desktop_here();
        let saved = helix_toolbox_engine::desktop::capture(name, &found)
            .ok_or_else(|| {
                "no desktop whose appearance Helix Toolbox can read is running here".to_string()
            })
            .and_then(|look| {
                let directory = self
                    .looks_directory()
                    .ok_or_else(|| "cannot find the configuration folder".to_string())?;
                helix_toolbox_engine::desktop::save(&directory, &look)
                    .map(|path| (look, path))
                    .map_err(|error| error.to_string())
            });
        self.popup = Some(Popup::Text(match saved {
            Ok((look, path)) => TextPopup::new(
                "Look saved",
                format!(
                    "{} ({} settings) is saved as {}. Enter on it under Saved looks puts it back.",
                    look.name,
                    look.settings.len(),
                    path.display()
                ),
                true,
            ),
            Err(error) => TextPopup::new("Look not saved", error, true),
        }));
        self.reload_desktop();
    }

    pub(in crate::tui::app) fn apply_look(&mut self, slug: &str) {
        let Some(directory) = self.looks_directory() else {
            return;
        };
        let Some((_, look)) = helix_toolbox_engine::desktop::load_all(&directory)
            .0
            .into_iter()
            .find(|(known, _)| known == slug)
        else {
            return;
        };
        let found = self.desktop_here();
        match helix_toolbox_engine::desktop::plan_apply(&look, &found) {
            Ok(planned) => self.confirm_plan(Ok(planned)),
            Err(why) => {
                self.popup = Some(Popup::Text(TextPopup::new(
                    "Cannot apply this look",
                    why,
                    true,
                )));
            }
        }
    }

    pub(in crate::tui::app) fn draw_desktop(&mut self, frame: &mut Frame, area: Rect) {
        let inner = self.page(
            frame,
            area,
            "Desktop",
            "The desktop, its login screen and look, and the entries for it; who else sets the look.",
        );
        self.fill(frame, inner);
        let focused = self.focus == Focus::Main;
        let items: Vec<Line> = self
            .desktop_rows
            .iter()
            .map(|row| match row {
                DesktopRow::Heading(title) => Line::styled(
                    title.clone(),
                    Style::default().fg(self.theme.group()).bold(),
                ),
                DesktopRow::Info { text, .. } => Line::from(vec![
                    Span::raw("  "),
                    Span::styled(
                        text.clone(),
                        Style::default().fg(
                            if text.starts_with("Note:") || text.starts_with("Not changed") {
                                self.theme.warning()
                            } else {
                                self.theme.entry()
                            },
                        ),
                    ),
                ]),
                DesktopRow::Entry { text, note, .. } => Line::from(vec![
                    Span::styled(
                        format!("  {} ", self.glyph(Icon::Entry)),
                        Style::default().fg(self.theme.muted()),
                    ),
                    Span::styled(text.clone(), Style::default().fg(self.theme.entry())),
                    Span::styled(
                        note.as_ref()
                            .map(|note| format!("  {note}"))
                            .unwrap_or_default(),
                        Style::default().fg(self.theme.faint()),
                    ),
                ]),
                DesktopRow::Look { text, .. } => Line::from(vec![
                    Span::styled(
                        format!("  {} ", self.glyph(Icon::Entry)),
                        Style::default().fg(self.theme.muted()),
                    ),
                    Span::styled(text.clone(), Style::default().fg(self.theme.entry())),
                ]),
            })
            .collect();
        let list = List::new(self.measure(items))
            .block(self.card("This machine"))
            .highlight_style(self.highlight(focused))
            .highlight_symbol(self.marker(focused))
            .scroll_padding(1);
        bordered_list(frame, list, inner, &mut self.main_state, &self.hits);
    }
}