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 convert_case::ccase;
use freedesktop_entry_parser::Entry;
use paste::paste;
use std::fmt::Write;
use tracing::{error, warn};

glib::wrapper! {
    pub struct UnitFileData(ObjectSubclass<imp::UnitFileDataImpl>);
}

macro_rules! write_attr {
    ($out:expr, $unit_file_data:expr, $member:ident) => {
        if let Some(value) = $unit_file_data.$member().as_deref() {
            let key = stringify!($member);
            let key = ccase!(pascal, key);
            let _ = writeln!($out, "{}={}", key, value.trim());
        }
    };
}

macro_rules! write_attr_values {
    ($out:expr, $unit_file_data:expr, $member:ident) => {
        for value in $unit_file_data.$member().iter() {
            let key = stringify!($member);
            let key = ccase!(pascal, key);
            let _ = writeln!($out, "{}={}", key, value.trim());
        }
    };
}

const VERSION: &str = env!("CARGO_PKG_VERSION");
const UNIT: &str = "Unit";
const SERVICE: &str = "Service";
impl UnitFileData {
    pub fn new() -> Self {
        let this_object: Self = glib::Object::new();
        this_object
    }

    pub fn to_file_data(&self) -> String {
        let mut out = format!("# Generated by SysD Manager {VERSION}\n");

        let mut sub_out = String::new();
        write_attr!(sub_out, self, description);

        if !sub_out.is_empty() {
            Self::write_section_header(&mut out, UNIT);
            out.push_str(&sub_out);
        }

        sub_out.clear();
        write_attr!(sub_out, self, working_directory);
        write_attr!(sub_out, self, exec_start);
        write_attr_values!(sub_out, self, environment);

        if !sub_out.is_empty() {
            Self::write_section_header(&mut out, SERVICE);
            out.push_str(&sub_out);
        }

        out
    }

    fn write_section_header(out: &mut String, section: &str) {
        let _ = writeln!(out, "\n[{}]", section);
    }

    pub fn from_file_data(data: &str) -> Self {
        let unit_file_data = UnitFileData::default();

        unit_file_data.update_file_data(data);

        unit_file_data
    }

    pub fn update_file_data(&self, data: &str) {
        let entry = match Entry::parse(data.as_bytes()) {
            Ok(entry) => entry,
            Err(err) => {
                error!("{:?}", err);
                return;
            }
        };

        for (section_name, section) in entry.sections() {
            for (key, values) in section.attrs() {
                fill_key_match(self, section_name, &key.key, values);
            }
        }
    }
}

macro_rules! match_pattern {
    ($unit_file_data:expr, $group:expr, $key:tt, $values:expr) => {
        if let Some(value) = $values.first() {
            paste! {
                $unit_file_data.[<set_ $key:snake>](value.as_str());
            }
        } else {
            warn!("{} has no values", $key);
        }
    };
}

macro_rules! match_group_key {
    ($unit_file_data:expr, $group:expr, $key:expr, $values:expr, $(($g:tt, $q:tt)),* $(,)?) => {
        match ($group, $key) {
            $( ($g, $q) => {
                match_pattern!($unit_file_data, $g, $q, $values);
            } )*
            (group_name, key) => {
                // warn!("Not Handle, Group {:?}, Key {:?}", group_name, key);
                fill_key_match_array($unit_file_data, group_name, key, $values);
            }
        }
    };
}

macro_rules! match_pattern_array {
    ($unit_file_data:expr, $group:expr, $key:tt, $values:expr) => {
        paste! {
            $unit_file_data.[<set_ $key:snake>]($values.to_owned());
        }
    };
}

macro_rules! match_group_key_array {
    ($unit_file_data:expr, $group:expr, $key:expr, $values:expr, $(($g:tt, $q:tt)),* $(,)?) => {
        match ($group, $key) {
            $( ($g, $q) => {
                match_pattern_array!($unit_file_data, $g, $q, $values);
            } )*
            (group_name, key) => {
                 warn!("Not Handle Array, Group {:?}, Key {:?}", group_name, key);
            }
        }
    };
}

fn fill_key_match(unit_file_data: &UnitFileData, group_name: &str, key: &str, values: &[String]) {
    match_group_key!(
        unit_file_data,
        group_name,
        key,
        values,
        ("Unit", "Description"),
        ("Service", "WorkingDirectory"),
        ("Service", "ExecStart"),
    );
}

fn fill_key_match_array(
    unit_file_data: &UnitFileData,
    group_name: &str,
    key: &str,
    values: &[String],
) {
    match_group_key_array!(
        unit_file_data,
        group_name,
        key,
        values,
        ("Service", "Environment")
    );
}

impl Default for UnitFileData {
    fn default() -> Self {
        Self::new()
    }
}

mod imp {
    use std::cell::RefCell;

    use crate::glib::subclass::{object::ObjectImpl, types::ObjectSubclass};
    use crate::gtk::{prelude::ObjectExt, subclass::prelude::DerivedObjectProperties};

    #[derive(Debug, glib::Properties, Default)]
    #[properties(wrapper_type = super::UnitFileData)]
    pub struct UnitFileDataImpl {
        #[property(get, set)]
        description: RefCell<Option<String>>,

        #[property(get, set)]
        exec_start: RefCell<Option<String>>,

        #[property(get, set)]
        working_directory: RefCell<Option<String>>,

        #[property(get, set)]
        environment: RefCell<Vec<String>>,

        #[property(get, set)]
        wanted_by: RefCell<Option<String>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for UnitFileDataImpl {
        const NAME: &'static str = "UnitFileData";
        type Type = super::UnitFileData;
        type ParentType = glib::Object;
        fn new() -> Self {
            Default::default()
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for UnitFileDataImpl {}
}

#[cfg(test)]
mod tests {
    use super::*;
    use test_base::init_logs;
    use tracing::info;

    #[test]
    fn out() {
        init_logs();
        let uf = UnitFileData::default();
        uf.set_description("asdf");
        uf.set_working_directory("/tmp/test/");

        info!("--Out put--\n{}", uf.to_file_data());
    }

    #[test]
    fn load() {
        init_logs();
        let uf = UnitFileData::default();
        uf.set_description(" this is a description ");
        uf.set_working_directory("/tmp/test/");
        uf.set_environment(vec!["KEY=VALUE".to_string(), "/path/to".to_string()]);

        let data = uf.to_file_data();
        info!("--Out put--\n{}", data);

        let uf2 = UnitFileData::from_file_data(&data);

        let data2 = uf2.to_file_data();
        info!("--Out put--\n{}", data2);
    }

    #[test]
    fn convert_case() {
        assert_eq!(ccase!(pascal, "working_directory"), "WorkingDirectory");
    }
}