sysd-manager 2.19.4

Application to empower user to manage their <b>systemd units</b> via Graphical User Interface. Not only are you able to make changes to the enablement and running status of each of the units, but you will also be able to view and modify their unit files and check the journal logs.
use crate::{
    systemd::{
        analyze::{self, Analyze},
        errors::SystemdErrors,
    },
    widget::{close_window_shortcut, unit_file_panel::flatpak},
};

use std::cell::Ref;

use adw::prelude::AdwDialogExt;
use gettextrs::pgettext;
use gtk::{
    gio::{self},
    glib::{self, BoxedAnyObject, object::Cast},
    pango::{AttrInt, AttrList, Weight},
    prelude::*,
};
use tracing::{error, info};

const PAGE_BLAME: &str = "blame";

pub fn build_analyze_window() -> Result<adw::Window, SystemdErrors> {
    let (analyse_box, store, total_time_label, stack) = build_analyze()?;
    let header = adw::HeaderBar::builder()
        .title_widget(&adw::WindowTitle::new(
            &pgettext("analyse blame", "Analyse Blame"),
            "",
        ))
        .css_classes(["raised"])
        .build();
    let toolbar = adw::ToolbarView::builder().content(&analyse_box).build();
    toolbar.add_top_bar(&header);
    let window = adw::Window::builder()
        .default_height(600)
        .default_width(600)
        .content(&toolbar)
        .build();

    close_window_shortcut(&window);
    window.connect_show(move |window| {
        fill_store(&store, &total_time_label, &stack, window);
    });

    Ok(window)
}

fn build_analyze() -> Result<(gtk::Box, gio::ListStore, gtk::Label, adw::ViewStack), SystemdErrors>
{
    // Analyse
    let unit_analyse_box = gtk::Box::builder()
        .orientation(gtk::Orientation::Vertical)
        .build();

    //label total
    let label_str = pgettext("analyse blame", "Total Time:");

    let total_time_box = gtk::Box::builder()
        .spacing(5)
        .halign(gtk::Align::Center)
        .build();

    total_time_box.append(&{
        let attribute_list = AttrList::new();
        attribute_list.insert(AttrInt::new_weight(Weight::Medium));
        gtk::Label::builder()
            .label(label_str)
            .attributes(&attribute_list)
            .build()
    });

    unit_analyse_box.append(&total_time_box);

    let attribute_list = AttrList::new();
    attribute_list.insert(AttrInt::new_weight(Weight::Medium));
    let total_time_label = gtk::Label::builder()
        //place holder, lees likely to be displaied
        .label(pgettext("analyse blame", "seconds ..."))
        .attributes(&attribute_list)
        .selectable(true)
        .focusable(false)
        .build();

    total_time_box.append(&total_time_label);
    // Setup the Analyze stack
    let (analyze_tree, store) = setup_systemd_analyze_tree()?;

    let unit_analyse_scrolled_window = gtk::ScrolledWindow::builder()
        .vexpand(true)
        .focusable(true)
        .child(&analyze_tree)
        .build();

    let stack = adw::ViewStack::new();
    let spinner = adw::Spinner::new();

    stack.add_named(&spinner, Some("spinner"));
    stack.add_named(&unit_analyse_scrolled_window, Some(PAGE_BLAME));

    // unit_analyse_box.append(&total_time_label);
    unit_analyse_box.append(&stack);

    Ok((unit_analyse_box, store, total_time_label, stack))
}

/// Use `systemd-analyze blame` to fill out the information for the Analyze `adw::ViewStack`.
fn setup_systemd_analyze_tree() -> Result<(gtk::ColumnView, gio::ListStore), SystemdErrors> {
    let store = gio::ListStore::new::<BoxedAnyObject>();

    let single_selection = gtk::SingleSelection::new(Some(store.clone()));

    let analyze_tree = gtk::ColumnView::builder()
        .focusable(true)
        .model(&single_selection)
        .hexpand(true)
        .build();

    let col1factory = gtk::SignalListItemFactory::new();
    let col2factory = gtk::SignalListItemFactory::new();

    col1factory.connect_setup(move |_factory, item| {
        let item = item.downcast_ref::<gtk::ListItem>().unwrap();
        let row = gtk::Inscription::default();
        item.set_child(Some(&row));
    });

    col1factory.connect_bind(move |_factory, item| {
        let item = item.downcast_ref::<gtk::ListItem>().unwrap();
        let child = item.child().and_downcast::<gtk::Inscription>().unwrap();
        let entry = item.item().and_downcast::<BoxedAnyObject>().unwrap();
        let r: Ref<Analyze> = entry.borrow();

        child.set_text(Some(r.time.to_string().as_str()));
    });

    col2factory.connect_setup(move |_factory, item| {
        let item = item.downcast_ref::<gtk::ListItem>().unwrap();
        let row = gtk::Inscription::default();
        item.set_child(Some(&row));
    });

    col2factory.connect_bind(move |_factory, item| {
        let item = item.downcast_ref::<gtk::ListItem>().unwrap();
        let child = item.child().and_downcast::<gtk::Inscription>().unwrap();
        let entry = item.item().and_downcast::<BoxedAnyObject>().unwrap();
        let r: Ref<Analyze> = entry.borrow();

        child.set_text(Some(r.service.to_string().as_str()));
    });

    let col1_time = gtk::ColumnViewColumn::new(
        //column header
        Some(&pgettext("analyse blame", "Init time (ms)")),
        Some(col1factory),
    );
    let col2_unit = gtk::ColumnViewColumn::new(
        //column header
        Some(&pgettext("analyse blame", "Running units")),
        Some(col2factory),
    );
    col2_unit.set_expand(true);

    analyze_tree.append_column(&col1_time);
    analyze_tree.append_column(&col2_unit);

    Ok((analyze_tree, store))
}

fn fill_store(
    list_store: &gio::ListStore,
    total_time_label: &gtk::Label,
    stack: &adw::ViewStack,
    window: &adw::Window,
) {
    {
        let list_store = list_store.clone();
        let total_time_label = total_time_label.clone();
        let stack = stack.clone();

        let window = window.clone();
        glib::spawn_future_local(async move {
            let (sender, receiver) = tokio::sync::oneshot::channel();
            systemd::runtime().spawn(async move {
                let response = analyze::blame().await;

                if let Err(e) = sender.send(response) {
                    error!("Channel closed unexpectedly: {e:?}");
                }
            });

            let Ok(response) = receiver
                .await
                .inspect_err(|err| error!("Tokio channel dropped {err:?}"))
            else {
                stack.set_visible_child_name(PAGE_BLAME);
                return;
            };

            list_store.remove_all();
            let mut time_full = 0;

            match response {
                Ok(units) => {
                    for value in units {
                        time_full = value.time;
                        list_store.append(&BoxedAnyObject::new(value));
                    }

                    info!("Unit list refreshed! list size {}", list_store.n_items());

                    let time = (time_full as f32) / 1000f32;

                    //total time
                    let total_time_label_str =
                        crate::format2!(pgettext("analyse blame", "{} seconds"), time);

                    total_time_label.set_label(&total_time_label_str);

                    stack.set_visible_child_name(PAGE_BLAME);
                }
                Err(error) => {
                    match error {
                        SystemdErrors::CmdNoFreedesktopFlatpakPermission(_, _) => {
                            let dialog = flatpak::flatpak_permision_alert();

                            dialog.present(Some(&window));

                            dialog.connect_closed(move |_dialog| window.close());
                            display_error(error, &stack);
                        }
                        SystemdErrors::CmdNoFlatpakSpawn => {
                            display_error(error, &stack);
                        }
                        _ => stack.set_visible_child_name(PAGE_BLAME),
                    };
                    stack.set_visible_child_name(PAGE_BLAME);
                }
            }
        });
    }
}

fn display_error(error: SystemdErrors, stack: &adw::ViewStack) {
    let tv = gtk::TextView::new();
    let buf = tv.buffer();

    let mut start_iter = buf.start_iter();
    let gui_description = error.gui_description().unwrap_or_default();
    buf.insert_markup(&mut start_iter, &gui_description);

    const FLATPACK_PERMISSION: &str = "flatpak_permission";
    stack.add_named(&tv, Some(FLATPACK_PERMISSION));
    stack.set_visible_child_name(FLATPACK_PERMISSION)
}