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
}
}
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) {
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>() {
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() {
file_dialog.set_initial_folder(Some(&file));
} else if path.is_file() {
file_dialog.set_initial_file(Some(&file));
}
} else {
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: >k::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")
);
}
}
}