use crate::{
systemd::{
self,
data::UnitInfo,
enums::{DisEnableFlags, StartStopMode},
errors::SystemdErrors,
},
systemd_gui,
widget::{
close_window_shortcut,
control_action_dialog::ControlActionType,
unit_control_panel::{UnitControlPanel, enums::UnitContolType},
},
};
use adw::{prelude::*, subclass::window::AdwWindowImpl};
use base::{
enums::UnitDBusLevel,
proxy::{DisEnAbleUnitFiles, DisEnAbleUnitFilesResponse},
};
use enumflags2::BitFlag;
use gio::glib::BoolError;
use gtk::{
glib::{self},
subclass::{
prelude::*,
widget::{
CompositeTemplateCallbacksClass, CompositeTemplateClass,
CompositeTemplateInitializingExt, WidgetImpl,
},
},
};
use std::cell::OnceCell;
use strum::IntoEnumIterator;
use tracing::error;
use tracing::{info, warn};
use super::ControlActionDialog;
const SAVE_CONTEXT_ENABLE_UNIT_FILE_RUNTIME: &str = "save-context-enable-unit-file-runtime";
const SAVE_CONTEXT_ENABLE_UNIT_FILE_FORCE: &str = "save-context-enable-unit-file-force";
const SAVE_CONTEXT_ENABLE_UNIT_FILE_RUN_NOW: &str = "save-context-enable-unit-file-run-now";
const SAVE_CONTEXT_ENABLE_UNIT_FILE_START_MODE: &str = "save-context-enable-unit-file-start-mode";
const SAVE_CONTEXT_ENABLE_UNIT_FILE_DBUS_LEVEL: &str = "save-context-enable-unit-file-dbus-level";
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/control_action_dialog.ui")]
pub struct EnableUnitDialogImp {
#[template_child]
send_action_button: TemplateChild<gtk::Button>,
#[template_child]
unit_file_entry: TemplateChild<adw::EntryRow>,
#[template_child]
runtime_switch: TemplateChild<adw::SwitchRow>,
#[template_child]
force_switch: TemplateChild<adw::SwitchRow>,
#[template_child]
portable_switch: TemplateChild<adw::SwitchRow>,
#[template_child]
run_stop_now_switch: TemplateChild<adw::SwitchRow>,
#[template_child]
run_stop_mode_combo: TemplateChild<adw::ComboRow>,
#[template_child]
dbus_level_combo: TemplateChild<adw::ComboRow>,
#[template_child]
use_selected_unit_button: TemplateChild<gtk::Button>,
#[template_child]
first_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
after_action_group: TemplateChild<adw::PreferencesGroup>,
#[template_child]
window_title: TemplateChild<adw::WindowTitle>,
action_type: OnceCell<ControlActionType>,
unit_control: OnceCell<UnitControlPanel>,
settings: OnceCell<gio::Settings>,
}
#[gtk::template_callbacks]
impl EnableUnitDialogImp {
#[template_callback]
fn send_action_button_clicked(&self, button: gtk::Button) {
let unit_file = self.unit_file_entry.text();
let unit_file2 = unit_file.clone();
let dialog = self.obj().clone();
let runtime = self.runtime_switch.is_active();
let force = self.force_switch.is_active();
let action_type = *self.action_type.get().expect("Value need to be set");
let Some(app_window) = self.unit_control.get().and_then(|ucp| ucp.app_window()) else {
error!("No App window");
return;
};
match action_type {
ControlActionType::EnableUnitFiles => {
let dbus_level = self.dbus_level_combo.selected();
let dbus_level: UnitDBusLevel = dbus_level.into();
let handling_response_callback = {
move |_method: &str,
_unit: Option<&UnitInfo>,
result: Result<DisEnAbleUnitFilesResponse, SystemdErrors>,
control: &UnitControlPanel| {
match result {
Ok(vec) => {
info!("{} Result: {:?}", action_type.code(), vec);
let unit_name = unit_file.as_str();
if dialog.imp().run_stop_now_switch.is_active() {
let mode = dialog.imp().run_stop_mode_combo.selected_item();
let start_mode: StartStopMode = object_start_stop_maode(mode);
info!(
"Try to start {unit_name:?} level: {dbus_level:?} mode: {start_mode:?}"
);
let start_results =
systemd::start_unit(dbus_level, unit_name, start_mode);
control.start_restart(
unit_name,
None,
start_results,
UnitContolType::Start,
start_mode,
);
}
match systemd::fetch_unit(dbus_level, unit_name) {
Ok(unit) => {
let returned_unit = app_window.set_unit(Some(&unit));
after_unit_file_action(
"",
returned_unit.as_ref(),
Ok(()),
control,
);
}
Err(e) => {
warn!(
"Enable unit fetch {unit_name:?} level {dbus_level:?} Error: {e:?}"
);
}
}
}
Err(_error) => {
}
}
}
};
let mut flags = DisEnableFlags::empty();
if force {
flags |= DisEnableFlags::SdSystemdUnitForce
}
if self.portable_switch.is_active() {
flags |= DisEnableFlags::SdSystemdUnitPortable
}
if runtime {
flags |= DisEnableFlags::SdSystemdUnitRuntime
}
let lambda = move |_params: Option<(UnitDBusLevel, String)>| {
systemd::enable_unit_file(dbus_level, unit_file2.as_str(), flags)
};
let unit_control = self.unit_control.get().expect("unit_control not None");
unit_control.call_method(
&action_type.method_name(),
false,
&button,
lambda,
handling_response_callback,
);
}
ControlActionType::DisableUnitFiles => {
let handling_response_callback = {
move |_method: &str,
unit: Option<&UnitInfo>,
result: Result<DisEnAbleUnitFilesResponse, SystemdErrors>,
control: &UnitControlPanel| {
match result {
Ok(ref vec) => {
info!("{} Result: {:?}", action_type.code(), vec);
if let Some(unit) = unit
&& dialog.imp().run_stop_now_switch.is_active()
{
let mode = dialog.imp().run_stop_mode_combo.selected_item();
let mode: StartStopMode = object_start_stop_maode(mode);
info!("Stop Unit {:?} mode {:?}", unit.primary(), mode);
let stop_results = systemd::stop_unit(
unit.dbus_level(),
&unit.primary(),
mode,
);
control.start_restart(
&unit.primary(),
Some(unit),
stop_results,
UnitContolType::Stop,
mode,
);
}
let result = result.map(|_arg| ());
after_unit_file_action(
&action_type.method_name(),
unit,
result,
control,
);
}
Err(_error) => {}
}
}
};
let mut flags = DisEnableFlags::empty();
if force {
flags |= DisEnableFlags::SdSystemdUnitForce
}
if self.portable_switch.is_active() {
flags |= DisEnableFlags::SdSystemdUnitPortable
}
if runtime {
flags |= DisEnableFlags::SdSystemdUnitRuntime
}
let lambda = move |params: Option<(UnitDBusLevel, String)>| {
if let Some((level, primary_name)) = params {
systemd::disable_unit_file(level, &primary_name, flags)
} else {
Err(SystemdErrors::NoUnit)
}
};
self.unit_control
.get()
.expect("unit_control not None")
.call_method(
&action_type.method_name(),
false,
&button,
lambda,
handling_response_callback,
);
}
ControlActionType::MaskUnit => {
let handling_response_callback = {
move |_method: &str,
unit: Option<&UnitInfo>,
result: Result<Vec<DisEnAbleUnitFiles>, SystemdErrors>,
control: &UnitControlPanel| {
match result {
Ok(ref vec) => {
info!("{} Result: {:?}", action_type.code(), vec);
if let Some(unit) = unit
&& dialog.imp().run_stop_now_switch.is_active()
{
let mode = dialog.imp().run_stop_mode_combo.selected_item();
let mode: StartStopMode = object_start_stop_maode(mode);
info!("Stop Unit {:?} mode {:?}", unit.primary(), mode);
let stop_results = systemd::stop_unit(
unit.dbus_level(),
&unit.primary(),
mode,
);
control.start_restart(
&unit.primary(),
Some(unit),
stop_results,
UnitContolType::Stop,
mode,
);
}
let result = result.map(|_arg| ());
after_unit_file_action("Mask", unit, result, control);
}
Err(_error) => {}
}
}
};
let lambda = move |params: Option<(UnitDBusLevel, String)>| {
if let Some((level, primary_name)) = params {
systemd::mask_unit_files(level, &primary_name, runtime, force)
} else {
Err(SystemdErrors::NoUnit)
}
};
self.unit_control
.get()
.expect("unit_control not None")
.call_method(
&action_type.method_name(),
false,
&button,
lambda,
handling_response_callback,
);
}
ControlActionType::Preset => {
let handling_response_callback = {
move |_method: &str,
unit: Option<&UnitInfo>,
result: Result<DisEnAbleUnitFilesResponse, SystemdErrors>,
control: &UnitControlPanel| {
match result {
Ok(ref vec) => {
info!("{} Result: {:?}", action_type.code(), vec);
let result = result.map(|_arg| ());
after_unit_file_action("Preset", unit, result, control);
}
Err(_error) => {}
}
}
};
let lambda = move |params: Option<(UnitDBusLevel, String)>| {
if let Some((level, primary_name)) = params {
systemd::preset_unit_files(level, &primary_name, runtime, force)
} else {
Err(SystemdErrors::NoUnit)
}
};
self.unit_control
.get()
.expect("unit_control not None")
.call_method(
&action_type.method_name(),
false,
&button,
lambda,
handling_response_callback,
);
}
ControlActionType::Reenable => {
let handling_response_callback = {
move |_method: &str,
unit: Option<&UnitInfo>,
result: Result<DisEnAbleUnitFilesResponse, SystemdErrors>,
control: &UnitControlPanel| {
match result {
Ok(ref vec) => {
info!("{} Result: {:?}", action_type.code(), vec);
let result = result.map(|_arg| ());
after_unit_file_action(
&action_type.method_name(),
unit,
result,
control,
);
}
Err(_error) => {}
}
}
};
let lambda = move |params: Option<(UnitDBusLevel, String)>| {
if let Some((level, primary_name)) = params {
systemd::reenable_unit_file(level, &primary_name, runtime, force)
} else {
Err(SystemdErrors::NoUnit)
}
};
self.unit_control
.get()
.expect("unit_control not None")
.call_method(
&action_type.method_name(),
false,
&button,
lambda,
handling_response_callback,
);
}
ControlActionType::Link => {
let dbus_level = self.dbus_level_combo.selected();
let dbus_level: UnitDBusLevel = dbus_level.into();
let handling_response_callback = {
move |_method: &str,
_unit: Option<&UnitInfo>,
result: Result<Vec<DisEnAbleUnitFiles>, SystemdErrors>,
control: &UnitControlPanel| {
match result {
Ok(vec) => {
info!("{} Result: {:?}", action_type.code(), vec);
let unit_name = unit_file.as_str();
match systemd::fetch_unit(dbus_level, unit_name) {
Ok(unit) => {
let returned_unit = app_window.set_unit(Some(&unit));
after_unit_file_action(
"",
returned_unit.as_ref(),
Ok(()),
control,
);
}
Err(e) => {
warn!(
"{} fetch {:?} level {:?} Error: {:?}",
action_type.code(),
unit_name,
dbus_level,
e
);
}
}
}
Err(_error) => {
}
}
}
};
let lambda = move |_params: Option<(UnitDBusLevel, String)>| {
systemd::link_unit_files(dbus_level, unit_file2.as_str(), runtime, force)
};
self.unit_control
.get()
.expect("unit_control not None")
.call_method(
&action_type.method_name(),
false,
&button,
lambda,
handling_response_callback,
);
}
}
}
#[template_callback]
fn unit_file_changed(&self, _entry: adw::EntryRow) {
info!("unit_file_changed");
self.set_send_button_sensitivity()
}
#[template_callback]
fn unit_file_apply(&self, _entry: adw::EntryRow) {
info!("unit_file_apply");
}
#[template_callback]
fn unit_file_entry_activated(&self, _entry: adw::EntryRow) {
info!("unit_file_entry_activated");
}
#[template_callback]
fn unit_file_delete_text(&self, a: i32, b: i32, _entry: adw::EntryRow) {
info!("unit_file_delete_text {a} {b}");
}
#[template_callback]
fn unit_file_insert_text(
&self,
text: &str,
position: i32,
pointer: glib::Value,
_entry: adw::EntryRow,
) {
info!("unit_file_insert_text {text:?} {position:?} {pointer:?}");
}
#[template_callback]
fn reset_button_clicked(&self) {
info!("reset_button_clicked");
let settings = self.settings.get().expect("setting nor None");
self.unit_file_entry.set_text("");
let dbus_level = settings.string(SAVE_CONTEXT_ENABLE_UNIT_FILE_DBUS_LEVEL);
let runtime = settings.boolean(SAVE_CONTEXT_ENABLE_UNIT_FILE_RUNTIME);
let force = settings.boolean(SAVE_CONTEXT_ENABLE_UNIT_FILE_FORCE);
let run_now = settings.boolean(SAVE_CONTEXT_ENABLE_UNIT_FILE_RUN_NOW);
let start_mode = settings.string(SAVE_CONTEXT_ENABLE_UNIT_FILE_START_MODE);
let dbus_level: UnitDBusLevel = dbus_level.as_str().into();
let position = dbus_level.value() as u32;
self.dbus_level_combo.set_selected(position);
self.runtime_switch.set_active(runtime);
self.force_switch.set_active(force);
self.run_stop_now_switch.set_active(run_now);
let start_mode: StartStopMode = start_mode.as_str().into();
let position = start_mode.discriminant();
self.run_stop_mode_combo.set_selected(position);
}
pub(crate) fn set_app_window(
&self,
unit_control: &UnitControlPanel,
action_type: super::ControlActionType,
) {
let _ = self.unit_control.set(unit_control.clone());
self.set_action_type(action_type);
self.set_unit(unit_control.current_unit())
}
#[template_callback]
fn file_browser_clicked(&self, _button: gtk::Button) {
let file_dialog = gtk::FileDialog::builder()
.title("Select a unit file")
.accept_label("Select")
.build();
let enable_unit_dialog = self.obj().clone();
let window: gtk::Window = enable_unit_dialog.clone().into();
file_dialog.open(
Some(&window),
None::<&gio::Cancellable>,
move |result| match result {
Ok(file) => {
if let Some(path) = file.path() {
let file_path_str = path.display().to_string();
enable_unit_dialog
.imp()
.unit_file_entry
.set_text(&file_path_str);
}
}
Err(e) => warn!("Unit File Selection Error {e:?}"),
},
);
}
#[template_callback]
fn use_selected_unit_clicked(&self, _button: gtk::Button) {
if let Some(selected_unit) = self.unit_control.get().and_then(|ucp| ucp.current_unit()) {
self.unit_file_entry.set_text(&selected_unit.primary());
}
}
fn set_send_button_sensitivity(&self) {
let unit_file = self.unit_file_entry.text();
self.send_action_button.set_sensitive(!unit_file.is_empty());
}
pub fn set_unit(&self, unit: Option<UnitInfo>) {
self.use_selected_unit_button.set_sensitive(unit.is_some());
if let Some(unit) = unit {
if self.action_type.get().expect("Not None").dialog_subtitle() {
self.window_title.set_subtitle(&unit.primary());
}
if !matches!(
self.action_type.get().expect("Not none"),
ControlActionType::EnableUnitFiles
) {
self.send_action_button.set_sensitive(true);
}
}
}
fn set_action_type(&self, action_type: super::ControlActionType) {
self.action_type.set(action_type).expect("Only set once");
self.window_title.set_title(&action_type.title());
self.first_group.set_title(&action_type.first_group_title());
self.after_action_group
.set_title(&action_type.after_group_title());
self.unit_file_entry
.set_visible(action_type.unit_file_entry_visible());
self.dbus_level_combo
.set_visible(action_type.dbus_level_combo_visible());
self.portable_switch
.set_visible(action_type.portable_switch_visible());
self.after_action_group
.set_visible(action_type.after_action_group_visible());
self.send_action_button
.set_label(&action_type.send_action_label());
let (title, subtitle) = action_type.run_stop_now();
self.run_stop_now_switch.set_title(&title);
self.run_stop_now_switch.set_subtitle(&subtitle);
let (title, subtitle) = action_type.run_stop_now_mode();
self.run_stop_mode_combo.set_title(&title);
self.run_stop_mode_combo.set_subtitle(&subtitle);
}
}
#[glib::object_subclass]
impl ObjectSubclass for EnableUnitDialogImp {
const NAME: &'static str = "ControlAction_DIALOG";
type Type = ControlActionDialog;
type ParentType = adw::Window;
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();
}
}
impl ObjectImpl for EnableUnitDialogImp {
fn constructed(&self) {
self.parent_constructed();
close_window_shortcut(self.obj().as_ref());
let settings = systemd_gui::new_settings();
self.settings
.set(settings.clone())
.expect("Settings set once only");
let model = adw::EnumListModel::new(StartStopMode::static_type());
let expression = gtk::PropertyExpression::new(
adw::EnumListItem::static_type(),
None::<gtk::Expression>,
"nick",
);
self.run_stop_mode_combo.set_expression(Some(expression));
self.run_stop_mode_combo.set_model(Some(&model));
let mut levels_string = Vec::new();
for level in UnitDBusLevel::iter() {
levels_string.push(level.nice_label());
}
let level_str: Vec<&str> = levels_string.iter().map(|x| &**x).collect();
let string_list = gtk::StringList::new(&level_str);
self.dbus_level_combo.set_model(Some(&string_list));
self.reset_button_clicked();
}
}
impl WidgetImpl for EnableUnitDialogImp {}
impl WindowImpl for EnableUnitDialogImp {
fn close_request(&self) -> glib::Propagation {
info!("Close window");
let dbus_level: UnitDBusLevel = self.dbus_level_combo.selected().into();
let runtime = self.runtime_switch.is_active();
let force = self.force_switch.is_active();
let run_now = self.run_stop_now_switch.is_active();
let start_mode = self.run_stop_mode_combo.selected_item();
let start_mode: StartStopMode = object_start_stop_maode(start_mode);
let settings = self.settings.get().expect("Settings not None");
fn settings_error(e: BoolError) {
error!("Setting error {e:?}");
}
let _ = settings
.set_string(
SAVE_CONTEXT_ENABLE_UNIT_FILE_DBUS_LEVEL,
dbus_level.as_str(),
)
.map_err(settings_error);
let _ = settings
.set_boolean(SAVE_CONTEXT_ENABLE_UNIT_FILE_RUNTIME, runtime)
.map_err(settings_error);
let _ = settings
.set_boolean(SAVE_CONTEXT_ENABLE_UNIT_FILE_FORCE, force)
.map_err(settings_error);
let _ = settings
.set_boolean(SAVE_CONTEXT_ENABLE_UNIT_FILE_RUN_NOW, run_now)
.map_err(settings_error);
let _ = settings
.set_string(
SAVE_CONTEXT_ENABLE_UNIT_FILE_START_MODE,
start_mode.as_str(),
)
.map_err(settings_error);
self.parent_close_request();
glib::Propagation::Proceed
}
}
impl AdwWindowImpl for EnableUnitDialogImp {}
pub fn after_unit_file_action(
_method_name: &str,
unit: Option<&UnitInfo>,
result: Result<(), SystemdErrors>,
control: &UnitControlPanel,
) {
if result.is_err() {
return;
}
let Some(unit) = unit else {
warn!("Unit None");
return;
};
let unit = unit.clone();
let control = control.clone();
glib::spawn_future_local(async move {
let _ = complete_unit_information(&unit).await;
control.selection_change(Some(&unit));
});
}
pub async fn complete_unit_information(unit: &UnitInfo) -> Result<(), SystemdErrors> {
let (sender, receiver) = tokio::sync::oneshot::channel();
let primary_name = unit.primary();
let level = unit.dbus_level();
let object_path = unit.object_path();
let status = unit.enable_status();
crate::systemd::runtime().spawn(async move {
let response =
systemd::complete_single_unit_information(primary_name, level, object_path, status)
.await;
if let Err(e) = sender.send(response) {
error!("Channel closed unexpectedly: {e:?}");
}
});
let Ok(receiver_result) = receiver.await else {
error!("Tokio channel dropped");
return Err(SystemdErrors::Custom("Channel dropped".to_string()));
};
let vec_unit_info =
receiver_result.inspect_err(|err| warn!("Fail to update Unit info {err:?}"))?;
if let Some(update) = vec_unit_info.into_iter().next() {
unit.update_from_unit_info(update);
}
Ok(())
}
fn object_start_stop_maode(value: Option<glib::Object>) -> StartStopMode {
let Some(object) = value else {
return StartStopMode::default();
};
let enum_list_item = object
.downcast::<adw::EnumListItem>()
.expect("Needs to be EnumListItem");
StartStopMode::from(enum_list_item.name().as_str())
}