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 glib::{WeakRef, subclass::types::ObjectSubclassIsExt};
use gtk::glib::{self};

use crate::widget::creator::UnitCreatorWindow;

glib::wrapper! {

    pub struct ServiceCreatorPage(ObjectSubclass<imp::ServiceCreatorPageImp>)
    @extends adw::NavigationPage,  gtk::Widget,
    @implements gtk::Accessible,  gtk::Buildable,  gtk::ConstraintTarget ;
}

impl ServiceCreatorPage {
    pub fn new(window: WeakRef<UnitCreatorWindow>) -> Self {
        let obj: ServiceCreatorPage = glib::Object::new();
        let _ = obj.imp().window.set(window);
        // obj.imp().update_from_unit_info();
        obj
    }
}

mod imp {

    use super::*;
    use crate::widget::creator::{CreateUnitErr, UnitCreateType, unit_file::UnitFileData};
    use adw::{prelude::PreferencesRowExt, subclass::prelude::*};
    use gtk::{glib, prelude::*};
    use std::{
        cell::{Cell, OnceCell},
        fs,
        os::unix::fs::PermissionsExt,
        path::Path,
    };
    use tracing::warn;

    #[derive(Default, gtk::CompositeTemplate, glib::Properties)]
    #[template(resource = "/io/github/plrigaux/sysd-manager/service_creator_page.ui")]
    #[properties(wrapper_type = super::ServiceCreatorPage)]
    pub struct ServiceCreatorPageImp {
        #[property(get, set, default)]
        creation_type: Cell<UnitCreateType>,

        #[template_child]
        description_entry: TemplateChild<adw::EntryRow>,

        #[template_child]
        exec_start_entry: TemplateChild<adw::EntryRow>,

        #[template_child]
        working_directory_entry: TemplateChild<adw::EntryRow>,

        pub(super) window: OnceCell<WeakRef<UnitCreatorWindow>>,

        #[property(get)]
        pub(super) data: OnceCell<UnitFileData>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for ServiceCreatorPageImp {
        const NAME: &'static str = "ServiceCreatorPage";
        type Type = ServiceCreatorPage;
        type ParentType = adw::NavigationPage;

        fn class_init(klass: &mut Self::Class) {
            // The layout manager determines how child widgets are laid out.
            klass.bind_template();
            klass.bind_template_callbacks();
        }

        fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for ServiceCreatorPageImp {
        fn constructed(&self) {
            self.parent_constructed();

            let data = self.data.get_or_init(UnitFileData::new);

            self.description_entry
                .bind_property("text", data, "description")
                .bidirectional()
                .build();

            self.exec_start_entry
                .bind_property("text", data, "exec_start")
                .bidirectional()
                .build();

            self.working_directory_entry
                .bind_property("text", data, "working_directory")
                .bidirectional()
                .build();

            let event_foc = gtk::EventControllerFocus::new();
            event_foc.connect_leave(|event| {
                if let Some(entry) = event.widget().and_downcast_ref::<adw::EntryRow>() {
                    // let text = entry.text();
                    ServiceCreatorPageImp::validate_entry_strat(entry);
                }
            });
            self.exec_start_entry.add_controller(event_foc);
        }
    }

    impl ServiceCreatorPageImp {
        fn validate_entry_strat(entry: &adw::EntryRow) {
            let text = entry.text();

            let name_err = match get_file_path(text.as_str()) {
                Ok(text) => {
                    if text.is_empty() {
                        CreateUnitErr::NoErr
                    } else {
                        let path = Path::new(text);

                        if !path.exists() {
                            CreateUnitErr::FileNotExits
                        } else if !path.is_file() {
                            CreateUnitErr::NotFile
                        } else if !is_executable(path) {
                            CreateUnitErr::NotExecutable
                        } else {
                            CreateUnitErr::NoErr
                        }
                    }
                }

                Err(err) => err,
            };

            match name_err {
                CreateUnitErr::NoErr => {
                    entry.remove_css_class("warning");
                }
                _ => {
                    entry.add_css_class("warning");
                }
            }
            let prefix = "ExecStart";
            entry.set_title(&name_err.title_err(prefix));
        }
    }

    fn is_executable(path: &Path) -> bool {
        let Ok(metadata) = fs::metadata(path) else {
            return false;
        };

        metadata.permissions().mode() & 0o111 != 0
    }

    #[gtk::template_callbacks]
    impl ServiceCreatorPageImp {
        #[template_callback]
        fn working_directory_search_dialog_clicked(&self, _button: gtk::Button) {
            let file_dialog = gtk::FileDialog::builder()
                .title("Select a working directory")
                .accept_label("Select")
                .build();

            let create_service_page = self.obj().clone();

            let text = self.working_directory_entry.text();
            let text = get_file_path(&text).unwrap_or_default();
            if text.is_empty() {
                set_initial_folder(&file_dialog);
            } else {
                let path = Path::new(text);
                if path.exists() {
                    let file = gio::File::for_path(path);
                    file_dialog.set_initial_file(Some(&file));
                } else {
                    println!("not ex {text}");
                    set_initial_folder(&file_dialog);
                }
            }

            let win = self.window.get().and_then(|w| w.upgrade());
            let win = win.and_upcast_ref::<gtk::Window>();

            file_dialog.select_folder(win, None::<&gio::Cancellable>, move |result| match result {
                Ok(file) => {
                    if let Some(path) = file.path() {
                        let file_path_str = path.display().to_string();
                        create_service_page
                            .imp()
                            .working_directory_entry
                            .set_text(&file_path_str);
                    }
                }
                Err(e) => warn!("Unit File Selection Error {e:?}"),
            });
        }

        #[template_callback]
        fn exec_start_dialog_clicked(&self, _button: gtk::Button) {
            let file_dialog = gtk::FileDialog::builder()
                .title("Select executable")
                .accept_label("Select")
                .build();

            let create_service_page = self.obj().clone();

            let text = self.exec_start_entry.text();
            let text = get_file_path(&text).unwrap_or_default();
            if text.is_empty() {
                set_initial_folder(&file_dialog);
            } else {
                let path = Path::new(text);
                if path.exists() {
                    let file = gio::File::for_path(path);
                    if path.is_dir() {
                        // println!("dir {:?} ", path);
                        file_dialog.set_initial_folder(Some(&file));
                    } else if path.is_file() {
                        // println!("file {:?} ", path);
                        file_dialog.set_initial_file(Some(&file));
                    }
                } else {
                    // println!("not ex");
                    set_initial_folder(&file_dialog);
                }
            }

            let win = self.window.get().and_then(|w| w.upgrade());
            let win = win.and_upcast_ref::<gtk::Window>();

            file_dialog.open(win, None::<&gio::Cancellable>, move |result| match result {
                Ok(file) => {
                    if let Some(path) = file.path() {
                        let mut file_path_str = path.display().to_string();
                        escape(&mut file_path_str);
                        create_service_page
                            .imp()
                            .exec_start_entry
                            .set_text(&file_path_str);
                    }
                }
                Err(e) => warn!("Unit File Selection Error {e:?}"),
            });
        }
    }

    impl WidgetImpl for ServiceCreatorPageImp {}

    impl NavigationPageImpl for ServiceCreatorPageImp {}

    fn escape(file_path: &mut String) {
        if file_path.contains(char::is_whitespace) {
            file_path.insert(0, '"');
            file_path.push('"');
        }
    }

    fn set_initial_folder(file_dialog: &gtk::FileDialog) {
        if let Ok(home) = std::env::var("HOME") {
            let path = Path::new(&home);
            let dir = gio::File::for_path(path);
            file_dialog.set_initial_folder(Some(&dir));
        }
    }

    fn get_file_path(text: &str) -> Result<&str, CreateUnitErr> {
        let text = text.trim_start();
        let mut begin = 0;
        let mut end = text.len();
        let mut in_quotes = false;

        for (idx, c) in text.char_indices() {
            if c.is_whitespace() && !in_quotes {
                end = idx;
                break;
            } else if c == '"' {
                if idx == 0 {
                    in_quotes = true;
                    begin = 1;
                } else {
                    end = idx;
                    in_quotes = false;
                    break;
                }
            }
        }
        if in_quotes {
            return Err(CreateUnitErr::Malformed);
        }
        Ok(&text[begin..end])
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn test_get_file() {
            assert_eq!(get_file_path("text"), Ok("text"));
            assert_eq!(get_file_path("  text"), Ok("text"));
            assert_eq!(get_file_path("  text   "), Ok("text"));
            assert_eq!(get_file_path("  text -f  "), Ok("text"));
            assert_eq!(get_file_path(r#""text asdf" xxx"#), Ok("text asdf"));
            assert_eq!(get_file_path("\"\"text"), Ok(""));

            assert_eq!(
                get_file_path("/home/plr/bin/AppDir/etc"),
                Ok("/home/plr/bin/AppDir/etc")
            );
        }
    }
}