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) => {
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");
}
}