use super::flatpak;
use crate::{
consts::{
ACTION_SAVE_UNIT_FILE, APP_ACTION_DAEMON_RELOAD_BUS, SETTING_FIND_IN_TEXT_OPEN,
UNIT_FILE_LINE_NUMBER_ACTION,
},
format2,
systemd::{
self, data::UnitInfo, errors::SystemdErrors, generate_file_uri, sysd_proxy_service_name,
},
systemd_gui::{self},
upgrade,
utils::font_management::set_text_view_font_display,
widget::{
InterPanelMessage,
app_window::AppWindow,
preferences::{data::PREFERENCES, style_scheme::set_new_style_scheme},
text_search::{self, on_new_text},
unit_file_panel::flatpak::PROCEED,
},
};
use adw::prelude::*;
use base::file::determine_drop_in_path_dir;
use gettextrs::{gettext, pgettext};
use gtk::{
TemplateChild,
ffi::GTK_INVALID_LIST_POSITION,
gio::SimpleAction,
glib,
subclass::{box_::BoxImpl, prelude::*},
};
use regex::Regex;
use sourceview5::{Buffer, prelude::*};
use std::{
cell::{Cell, OnceCell, RefCell},
ffi::OsStr,
fmt::Write,
path::Path,
};
use tracing::{debug, error, info, warn};
const PANEL_EMPTY: &str = "empty";
const PANEL_FILE: &str = "file_panel";
const DEFAULT_DROP_IN_FILE_NAME: &str = "override";
const UNIT_FILE_ID: &str = "unit_file";
#[derive(PartialEq, Copy, Clone)]
enum UnitFileStatus {
Create,
Edit,
}
#[derive(Clone)]
struct FileNav {
file_path: String,
id: String,
status: UnitFileStatus,
is_drop_in: bool,
is_runtime: bool,
}
impl FileNav {
fn is_file(&self) -> bool {
!self.is_drop_in
}
fn file_stem(&self) -> Option<&str> {
Path::new(&self.file_path)
.file_stem()
.and_then(OsStr::to_str)
}
fn file_path_cloned(self, file_path: String) -> FileNav {
FileNav { file_path, ..self }
}
}
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/unit_file_panel.ui")]
pub struct UnitFilePanelImp {
unit_file_text: OnceCell<sourceview5::View>,
sourceview5_buffer: OnceCell<sourceview5::Buffer>,
#[template_child]
unit_file_scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
file_link: TemplateChild<gtk::LinkButton>,
#[template_child]
panel_file_stack: TemplateChild<adw::ViewStack>,
#[template_child]
file_dropin_selector: TemplateChild<adw::ToggleGroup>,
#[template_child]
unit_file_menu: TemplateChild<gio::MenuModel>,
#[template_child]
text_search_bar: TemplateChild<gtk::SearchBar>,
#[template_child]
find_text_button: TemplateChild<gtk::ToggleButton>,
app_window: OnceCell<AppWindow>,
visible_on_page: Cell<bool>,
unit: RefCell<Option<UnitInfo>>,
unit_dependencies_loaded: Cell<bool>,
all_unit_files: RefCell<Vec<FileNav>>,
file_content_selected_index: Cell<u32>,
original_file_content: RefCell<String>,
}
macro_rules! get_buffer {
($self:expr) => {{
let buffer = $self
.unit_file_text
.get()
.expect("unit_file_text shall be set")
.buffer();
buffer.downcast::<Buffer>().expect("suppose to be Buffer")
}};
}
#[gtk::template_callbacks]
impl UnitFilePanelImp {
fn save_file(&self) {
let binding = self.unit.borrow();
let Some(unit) = binding.as_ref() else {
warn!("no unit file");
return;
};
let buffer = self
.unit_file_text
.get()
.expect("expect sourceview5::View")
.buffer();
let start = buffer.start_iter();
let end = buffer.end_iter();
let text = buffer.text(&start, &end, true);
let binding = self.all_unit_files.borrow();
let index = self.file_content_selected_index.get() as usize;
let Some(mut file_nav) = binding.get(index).cloned() else {
warn!("No file path to save");
return;
};
let file_panel = self.obj().clone();
let level = unit.dbus_level();
let unit_name = unit.primary();
let unit_name2 = unit.primary();
let user_session = level.user_session();
match file_nav.status {
UnitFileStatus::Create => {
let (cleaned_text, file_stem) =
Self::clean_create_text(&unit.primary(), text.as_str());
let file_stem = if let Some(file_stem) = file_stem {
file_stem
} else {
DEFAULT_DROP_IN_FILE_NAME.to_owned()
};
let unique_drop_in_stem = self.unique_drop_in_stem(&file_stem);
glib::spawn_future_local(async move {
let (sender, receiver) = tokio::sync::oneshot::channel();
systemd::runtime().spawn(async move {
let response = systemd::create_drop_in(
user_session,
file_nav.is_runtime,
&unit_name,
&unique_drop_in_stem,
&cleaned_text,
)
.await;
if let Err(e) = sender.send(response) {
error!("Channel closed unexpectedly: {e:?}");
}
});
let Ok(response) = receiver
.await
.inspect_err(|err| error!("Tokio channel dropped {err:?}"))
else {
return;
};
if let Ok(ref file_path) = response {
file_nav = file_nav.file_path_cloned(file_path.clone());
file_panel.imp().all_unit_files.borrow_mut()[index] = file_nav.clone();
}
file_panel
.imp()
.handle_save_response(response, file_nav, &unit_name2, user_session)
.await;
});
}
UnitFileStatus::Edit => {
let file_path = file_nav.file_path.clone();
glib::spawn_future_local(async move {
let (sender, receiver) = tokio::sync::oneshot::channel();
let content = remove_trailing_newlines(&text)
.inspect_err(|e| warn!("{e:?}"))
.unwrap_or(text.to_string());
systemd::runtime().spawn(async move {
let response = systemd::save_file(level, &file_path, &content).await;
if let Err(e) = sender.send(response) {
error!("Channel closed unexpectedly: {e:?}");
}
});
let Ok(response) = receiver.await else {
error!("Tokio channel dropped");
return;
};
file_panel
.imp()
.handle_save_response(response, file_nav, &unit_name2, user_session)
.await;
});
}
}
}
async fn handle_save_response<T>(
&self,
receiver: Result<T, SystemdErrors>,
file_nav: FileNav,
unit_name: &str,
user_session: bool,
) {
let (msg, use_mark_up, action) = match receiver {
Ok(_) => {
self.set_save_file_enable(false);
let msg = match file_nav.status {
UnitFileStatus::Create => pgettext("file", "File {} created successfully!"),
UnitFileStatus::Edit => pgettext("file", "File {} saved successfully!"),
};
let file_path_format = format!("<u>{}</u>", file_nav.file_path);
let msg = format2!(msg, file_path_format);
if file_nav.status == UnitFileStatus::Create {
self.display_unit_file_content(Some(&file_nav), unit_name);
}
let button_label = gettext("Daemon Reload");
(
msg,
true,
Some((APP_ACTION_DAEMON_RELOAD_BUS, button_label, user_session)),
)
}
Err(error) => {
warn!(
"Unit {:?}, Unable to save file: {:?}, Error {:?}",
unit_name, file_nav.file_path, error
);
match error {
SystemdErrors::NotAuthorized => (
pgettext("file", "Not able to save file, permission not granted!"),
false,
None,
),
SystemdErrors::ZFdoServiceUnknowm(_s) => {
let service_name = sysd_proxy_service_name();
let app_window = self.app_window.get();
let dialog = flatpak::proxy_service_not_started(&service_name, app_window);
let window = self.app_window.get().expect("AppWindow supposed to be set");
dialog.present(Some(window));
(
pgettext("file", "Not able to save file, permission not granted!"),
false,
None,
)
}
SystemdErrors::CmdNoFreedesktopFlatpakPermission(_, _) => {
let dialog = flatpak::flatpak_permision_alert();
dialog.present(self.app_window.get());
(
pgettext(
"file",
"Not able to save file, Flatpak permission not granted!",
),
false,
None,
)
}
_ => (
pgettext("file", "Not able to save file, an error happened!"),
false,
None,
),
}
}
};
self.add_toast_message(&msg, use_mark_up, action);
}
}
macro_rules! get_unit {
($self:expr) => {{
let binding = $self.unit.borrow();
let Some(unit) = binding.as_ref() else {
warn!("No unit to present");
$self.set_editor_text(String::default(), false);
return;
};
unit.clone()
}};
}
impl UnitFilePanelImp {
fn clean_create_text(unit_name: &str, text: &str) -> (String, Option<String>) {
let mut cleaned_text = String::new();
let re_str = format!(r"/(run|etc)/systemd/system/{}.d/(.+).conf$", unit_name);
let re = Regex::new(&re_str).expect("Valid RegEx");
let mut content = false;
let mut file_name: Option<_> = None;
for line in text.lines() {
if !line.starts_with("###") {
content = true;
let trimmed_line = line.trim_end();
cleaned_text.push_str(trimmed_line);
if content {
writeln!(cleaned_text).expect("Writing to string should work");
}
} else if content {
break;
} else if let Some(caps) = re.captures(line) {
file_name = Some(caps[2].to_string());
}
}
cleaned_text = remove_trailing_newlines(&cleaned_text)
.inspect_err(|e| warn!("{e:?}"))
.unwrap_or(cleaned_text);
(cleaned_text, file_name)
}
fn add_toast_message(&self, message: &str, markup: bool, action: Option<(&str, String, bool)>) {
if let Some(app_window) = self.app_window.get() {
app_window.add_toast_message(message, markup, action);
}
}
fn set_visible_on_page(&self, value: bool) {
debug!("set_visible_on_page val {value}");
self.visible_on_page.set(value);
if self.visible_on_page.get()
&& !self.unit_dependencies_loaded.get()
&& self.unit.borrow().is_some()
{
self.set_file_content_init()
}
}
fn set_unit(&self, unit: Option<&UnitInfo>) {
let Some(unit) = unit else {
self.file_content_selected_index.set(0);
self.unit.replace(None);
self.set_file_content_init();
return;
};
let old_unit = self.unit.replace(Some(unit.clone()));
if let Some(old_unit) = old_unit
&& old_unit.primary() != unit.primary()
{
self.unit_dependencies_loaded.set(false)
}
self.file_content_selected_index.set(0);
self.set_file_content_init()
}
pub fn set_file_content_init(&self) {
if !self.visible_on_page.get() {
return;
}
let unit = get_unit!(self);
let object_path = unit.object_path();
let level = unit.dbus_level();
let unit_file_panel = self.obj().clone();
glib::spawn_future_local(async move {
let (sender, receiver) = tokio::sync::oneshot::channel();
crate::systemd::runtime().spawn(async move {
let response = systemd::fetch_drop_in_paths(level, &object_path).await;
if let Err(e) = sender.send(response) {
error!("Channel closed unexpectedly: {e:?}");
}
});
let Ok(result) = receiver.await else {
error!("Tokio channel dropped");
return;
};
match result {
Ok(drop_in_files) => {
unit_file_panel.imp().set_dropins(&drop_in_files);
}
Err(err) => {
warn!("Fail to update Unit info {err:?}");
}
};
});
let primary = unit.primary();
self.set_dropins(&[]);
self.display_unit_file_content(None, &primary);
}
fn display_unit_file_content(&self, file_nav: Option<&FileNav>, unit_name: &str) {
if let Some(file_nav) = file_nav {
self.display_unit_file_content2(unit_name, file_nav);
} else {
let all_files = self.all_unit_files.borrow();
if all_files.is_empty() {
self.fill_gui_content(String::new(), false, "");
return;
}
let file_nav = all_files.first().expect("vector should not be empty");
self.display_unit_file_content2(unit_name, file_nav);
};
}
fn display_unit_file_content2(&self, unit_name: &str, file_nav: &FileNav) {
let (file_content, is_error_msg) =
systemd::fetch_unit_file_content(Some(&file_nav.file_path), unit_name)
.map(|content| (content, false))
.unwrap_or_else(|e| {
warn!("File Content Error: {e:?}");
if e.file_not_found() {
(
pgettext(
"file",
"File not found! You may need to perform \"Daemon Reload\"",
),
true,
)
} else {
#[cfg(feature = "flatpak")]
{
let mut body = String::new();
body.push_str(
"You may miss a permission to be able to read the file.\n\n",
);
body.push_str(
"To know how to acquire needed permissions, follow this link:\n\n\
https://github.com/plrigaux/sysd-manager/wiki/Flatpak",
);
(body, true)
}
#[cfg(not(feature = "flatpak"))]
(String::new(), true)
}
});
self.fill_gui_content(file_content, is_error_msg, &file_nav.file_path);
}
fn fill_gui_content(&self, file_content: String, is_error_msg: bool, file_path: &str) {
let uri = generate_file_uri(file_path);
self.file_link.set_uri(&uri);
self.file_link.set_label(file_path);
self.set_editor_text(file_content, is_error_msg);
}
fn display_unit_drop_in_file_content(&self, drop_in_index: u32) {
let binding = self.all_unit_files.borrow();
let Some(file_nav) = binding.get(drop_in_index as usize) else {
warn!(
"Drop in index out of bound requested: {drop_in_index} max: {}",
self.all_unit_files.borrow().len()
);
self.set_editor_text(String::default(), false);
return;
};
let unit = get_unit!(self);
let primary = unit.primary();
self.display_unit_file_content(Some(file_nav), &primary);
}
fn set_dropins(&self, drop_in_files: &[String]) {
{
let mut all_files = self.all_unit_files.borrow_mut();
all_files.clear();
if let Some(file_path) = get_unit!(self).file_path() {
let fnav = FileNav {
file_path,
id: UNIT_FILE_ID.to_string(),
status: UnitFileStatus::Edit,
is_drop_in: false,
is_runtime: false, };
all_files.push(fnav);
}
for (idx, drop_in_file) in drop_in_files.iter().enumerate() {
let name = format!("dropin {idx}");
let fnav = FileNav {
file_path: drop_in_file.clone(),
id: name,
status: UnitFileStatus::Edit,
is_drop_in: true,
is_runtime: drop_in_file.starts_with("/run"),
};
all_files.push(fnav);
}
}
self.set_drop_ins_selector();
}
fn set_drop_ins_selector(&self) {
self.file_dropin_selector.remove_all();
let all_files = self.all_unit_files.borrow();
let all_files_len = all_files.len();
let mut idx = 1;
for file_nav in all_files.iter() {
let label_text = if file_nav.is_file() {
pgettext("file", "Unit File")
} else {
let label_text = pgettext("file", "Drop In");
if all_files_len > 2 {
let label = format!("{label_text} {idx}");
idx += 1;
label
} else {
label_text
}
};
let toggle = adw::Toggle::builder()
.label(&label_text)
.name(&file_nav.id)
.tooltip(file_nav.file_path.clone())
.build();
self.file_dropin_selector.add(toggle);
}
let visible = all_files_len > 1;
self.file_dropin_selector.set_visible(visible);
self.set_visible_child_panel();
}
fn file_dropin_selector_activate(&self, selected_index: u32) {
if self.file_content_selected_index.get() == selected_index {
return;
}
self.file_content_selected_index.set(selected_index);
self.display_unit_drop_in_file_content(selected_index);
}
fn set_editor_text(&self, file_content: String, is_error_msg: bool) {
let view = self.unit_file_text.get().expect("expect sourceview5::View");
let buf = view.buffer();
if let Some(buffer) = buf.downcast_ref::<Buffer>() {
if is_error_msg {
buffer.set_language(None);
} else if buffer.language().is_none()
&& let Some(ref language) = sourceview5::LanguageManager::new().language("ini")
{
buffer.set_language(Some(language));
}
}
buf.set_text(""); buf.set_text(&file_content);
self.original_file_content.replace(file_content);
self.set_visible_child_panel();
on_new_text(&self.text_search_bar);
}
fn set_visible_child_panel(&self) {
let panel = if self.all_unit_files.borrow().is_empty() {
PANEL_EMPTY
} else {
PANEL_FILE
};
self.panel_file_stack.set_visible_child_name(panel);
}
fn set_dark(&self, is_dark: bool) {
let style_scheme_id = PREFERENCES.unit_file_style_scheme();
debug!("File Unit set_dark {is_dark} style_scheme_id {style_scheme_id:?}");
let buffer = get_buffer!(self);
set_new_style_scheme(&buffer, Some(&style_scheme_id));
}
pub(crate) fn register(&self, app_window: &AppWindow) {
if let Err(err) = self.app_window.set(app_window.clone()) {
error!("Error {:?}", err);
return;
}
let rename_drop_in_file = gio::ActionEntry::builder("rename_drop_in_file")
.activate(move |_application: &AppWindow, _b, _target_value| {
info!("call rename_drop_in_file");
})
.build();
let create_drop_in_file_runtime = {
let unit_file_panel = self.obj().clone();
gio::ActionEntry::builder("create_drop_in_file_runtime")
.activate(
move |_application: &AppWindow, _b: &SimpleAction, _target_value| {
info!("call create_drop_in_file_runtime");
let _ = unit_file_panel
.imp()
.create_drop_in_file(true)
.inspect_err(|e| warn!("{e:?}"));
},
)
.build()
};
let create_drop_in_file_permanent = {
let unit_file_panel = self.obj().clone();
gio::ActionEntry::builder("create_drop_in_file_permanent")
.activate(
move |_application: &AppWindow, _b: &SimpleAction, _target_value| {
info!("call create_drop_in_file_permanent");
let _ = unit_file_panel
.imp()
.create_drop_in_file(false)
.inspect_err(|e| warn!("{e:?}"));
},
)
.build()
};
let revert_unit_file_full = {
let unit_file_panel = self.obj().clone();
gio::ActionEntry::builder("revert_unit_file_full")
.activate(
move |_application: &AppWindow, _b: &SimpleAction, _target_value| {
info!("call revert_unit_file_full");
let _ = unit_file_panel
.imp()
.revert_unit_file_full()
.inspect_err(|e| warn!("{e:?}"));
},
)
.build()
};
let save_unit_file = {
let unit_file_panel = self.obj().clone();
gio::ActionEntry::builder(ACTION_SAVE_UNIT_FILE)
.activate(move |_application: &AppWindow, _, _| {
unit_file_panel.imp().save_file();
})
.build()
};
app_window.add_action_entries([
rename_drop_in_file,
create_drop_in_file_runtime,
create_drop_in_file_permanent,
revert_unit_file_full,
save_unit_file,
]);
self.set_save_file_enable(false);
let settings = systemd_gui::new_settings();
let action = settings.create_action(&UNIT_FILE_LINE_NUMBER_ACTION[4..]);
app_window.add_action(&action);
settings
.bind::<sourceview5::View>(
&UNIT_FILE_LINE_NUMBER_ACTION[4..],
self.unit_file_text.get().unwrap(),
"show_line_numbers",
)
.build();
app_window.add_action(&action);
settings
.bind::<gtk::SearchBar>(
SETTING_FIND_IN_TEXT_OPEN,
&self.text_search_bar,
"search-mode-enabled",
)
.build();
}
fn set_save_file_enable(&self, enable: bool) {
if let Some(app_window) = self.app_window.get()
&& let Some(action) = app_window.lookup_action(ACTION_SAVE_UNIT_FILE)
&& let Some(simple_action) = action.downcast_ref::<gio::SimpleAction>()
&& simple_action.is_enabled() != enable
{
info!("Enable Save File Action {enable}");
simple_action.set_enabled(enable);
}
}
fn refresh_panels(&self, _unit: Option<&UnitInfo>) {
if self.visible_on_page.get() {
self.set_file_content_init()
}
}
fn create_drop_in_file(&self, runtime: bool) -> Result<(), SystemdErrors> {
info!("create_drop_in_file called runtime {runtime}");
let binding = self.unit.borrow();
let Some(unit) = binding.as_ref() else {
warn!("no unit file");
return Ok(());
};
let file_path = unit.file_path();
let primary = unit.primary();
let file_content = systemd::fetch_unit_file_content(file_path.as_deref(), &primary)
.unwrap_or_else(|e| {
warn!("get_unit_file_info Error: {e:?}");
"".to_owned()
});
let user_session = unit.dbus_level().user_session();
let drop_in_file_path = self.create_drop_in_file_path(&primary, runtime, user_session)?;
self.create_drop_in_nav(&drop_in_file_path, runtime);
self.set_drop_ins_selector();
self.file_dropin_selector
.set_active(self.file_dropin_selector.n_toggles() - 1);
let new_file_content: String = self
.set_dropin_file_format(file_path, file_content, &drop_in_file_path)
.inspect_err(|e| warn!("some error {:?}", e))
.unwrap_or_default();
self.fill_gui_content(new_file_content, false, &drop_in_file_path);
Ok(())
}
fn create_drop_in_nav(&self, drop_in_file_path: &str, runtime: bool) {
let fnav = FileNav {
file_path: drop_in_file_path.to_string(),
id: "create drop".to_string(),
status: UnitFileStatus::Create,
is_drop_in: true,
is_runtime: runtime,
};
self.all_unit_files.borrow_mut().push(fnav);
}
fn create_drop_in_file_path(
&self,
primary: &str,
runtime: bool,
user_session: bool,
) -> Result<String, SystemdErrors> {
let mut path_dir = determine_drop_in_path_dir(primary, runtime, user_session)?;
let drop_in_stem = self.unique_drop_in_stem(DEFAULT_DROP_IN_FILE_NAME);
path_dir.push('/');
path_dir.push_str(&drop_in_stem);
path_dir.push_str(".conf");
Ok(path_dir)
}
fn unique_drop_in_stem(&self, file_stem: &str) -> String {
let all_unit_files = self.all_unit_files.borrow();
let (file_stem, mut idx) = Self::grab_index(file_stem);
loop {
let file_stem = if idx == 0 {
file_stem.to_string()
} else {
format!("{}-{}", file_stem, idx)
};
if all_unit_files.iter().any(|f| {
f.is_drop_in
&& f.status != UnitFileStatus::Create
&& f.file_stem() == Some(&file_stem)
}) {
idx += 1;
continue;
}
return file_stem;
}
}
fn grab_index(file_stem: &str) -> (&str, u32) {
let re = Regex::new(r"-(\d+)$").expect("Valid RegEx");
if let Some(caps) = re.captures(file_stem) {
let start = caps.get_match().start();
if let Ok(num) = caps[1].parse::<u32>() {
return (&file_stem[0..start], num + 1);
}
}
(file_stem, 0)
}
fn set_dropin_file_format(
&self,
file_path: Option<String>,
file_content: String,
drop_in_file_path: &str,
) -> Result<String, SystemdErrors> {
let mut new_file_content = String::with_capacity(file_content.len() * 2);
writeln!(
new_file_content,
"### {} {}",
pgettext("file", "Editing"),
drop_in_file_path
)?;
writeln!(
new_file_content,
"### {}",
pgettext(
"file",
"Note: you can change the drop-in file name by modifying the above line"
)
)?;
writeln!(
new_file_content,
"###\n### {}",
pgettext(
"file",
"Anything between here and the comment below will become the contents of the drop-in file"
)
)?;
new_file_content.push_str("\n\n\n");
writeln!(
new_file_content,
"### {}",
pgettext("file", "Edits below this comment will be discarded")
)?;
new_file_content.push('\n');
writeln!(new_file_content, "### {}", file_path.unwrap_or_default())?;
for line in file_content.lines() {
new_file_content.push_str("# ");
new_file_content.push_str(line);
new_file_content.push('\n');
}
Ok(new_file_content)
}
pub(super) fn set_inter_message(&self, action: &InterPanelMessage) {
match *action {
InterPanelMessage::FontProvider(old, new) => {
let view = self.unit_file_text.get().expect("expect sourceview5::View");
set_text_view_font_display(old, new, &view.display())
}
InterPanelMessage::IsDark(is_dark) => self.set_dark(is_dark),
InterPanelMessage::PanelVisible(visible) => self.set_visible_on_page(visible),
InterPanelMessage::NewStyleScheme(style_scheme) => {
let buffer = get_buffer!(self);
set_new_style_scheme(&buffer, style_scheme);
}
InterPanelMessage::UnitChange(unit) => self.set_unit(unit),
InterPanelMessage::Refresh(unit) => self.refresh_panels(unit),
_ => {}
}
}
fn revert_unit_file_full(&self) -> Result<(), SystemdErrors> {
let binding = self.unit.borrow();
let Some(unit) = binding.as_ref() else {
return Err(SystemdErrors::NoUnit);
};
let unit_name = unit.primary();
let file_panel = self.obj().clone();
let dialog = flatpak::revert_drop_in_alert(&unit_name);
dialog.connect_response(None, move |_dialog, response| {
info!("Response {response}");
if response == PROCEED {
let _ = file_panel.imp().revert_unit_file_full_action();
}
});
let window = self.app_window.get();
if window.is_none() {
warn!("AppWindow supposed to be set");
}
dialog.present(window);
Ok(())
}
fn revert_unit_file_full_action(&self) -> Result<(), SystemdErrors> {
let binding = self.unit.borrow();
let Some(unit) = binding.as_ref() else {
return Err(SystemdErrors::NoUnit);
};
let file_panel = self.obj().clone();
let level = unit.dbus_level();
let unit_name = unit.primary();
glib::spawn_future_local(async move {
let unit_name2 = unit_name.clone();
let (sender, receiver) = tokio::sync::oneshot::channel();
systemd::runtime().spawn(async move {
let response = systemd::revert_unit_file_full(level, &unit_name).await;
info!("revert_unit_file_full results {:?}", response);
if let Err(e) = sender.send(response) {
error!("Channel closed unexpectedly: {e:?}");
}
});
let Ok(result) = receiver.await else {
error!("Tokio channel dropped");
return;
};
let (msg, use_mark_up, action) = match result {
Ok(_a) => {
let msg = pgettext("file", "Unit {} reverted successfully!");
let file_path_format = format!("<unit>{}</unit>", unit_name2);
let msg = format2!(msg, file_path_format);
file_panel.imp().set_dropins(&[]);
let button_label = gettext("Daemon Reload");
(
msg,
true,
Some((
APP_ACTION_DAEMON_RELOAD_BUS,
button_label,
level.user_session(),
)),
)
}
Err(error) => {
warn!("Unit {:?}, Unable to revert {:?}", unit_name2, error);
match error {
SystemdErrors::NotAuthorized => (
pgettext("file", "Not able to save file, permission not granted!"),
false,
None,
),
SystemdErrors::ZFdoServiceUnknowm(_s) => {
let service_name = sysd_proxy_service_name();
let app_window = file_panel.imp().app_window.get();
let dialog =
flatpak::proxy_service_not_started(&service_name, app_window);
let window = file_panel
.imp()
.app_window
.get()
.expect("AppWindow supposed to be set");
dialog.present(Some(window));
(
pgettext(
"file",
"Not able to revert unit, permission not granted!",
),
false,
None,
)
}
_ => (
pgettext("file", "Not able to revert unit, an error happened!"),
false,
None,
),
}
}
};
file_panel
.imp()
.add_toast_message(&msg, use_mark_up, action);
});
Ok(())
}
pub(crate) fn focus_text_search(&self) {
text_search::focus_on_text_entry(&self.text_search_bar)
}
}
#[glib::object_subclass]
impl ObjectSubclass for UnitFilePanelImp {
const NAME: &'static str = "UnitFilePanel";
type Type = super::UnitFilePanel;
type ParentType = gtk::Box;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
klass.install_action("test_pizza", None, |a, b, c| {
debug!("test a {:?} b {:?} c {:?}", a, b, c)
});
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for UnitFilePanelImp {
fn constructed(&self) {
self.parent_constructed();
self.set_visible_child_panel();
let buffer = sourceview5::Buffer::new(None);
if let Some(ref language) = sourceview5::LanguageManager::new().language("ini") {
buffer.set_language(Some(language));
}
let view = sourceview5::View::with_buffer(&buffer);
view.set_show_line_numbers(true);
view.set_highlight_current_line(true);
view.set_tab_width(4);
view.set_monospace(true);
view.set_wrap_mode(gtk::WrapMode::WordChar);
self.unit_file_scrolled_window.set_child(Some(&view));
{
let buffer = view.buffer();
let unit_file_panel = self.obj().downgrade();
buffer.connect_end_user_action(move |buf| {
let unit_file_panel = upgrade!(unit_file_panel);
let imp = unit_file_panel.imp();
let start = buf.start_iter();
let end = buf.end_iter();
let current_text = buf.text(&start, &end, true);
let allow_save_condition = !imp.all_unit_files.borrow().is_empty()
&& current_text.as_str() != imp.original_file_content.borrow().as_str();
imp.set_save_file_enable(allow_save_condition);
});
}
let file_text_view = view.upcast_ref::<gtk::TextView>();
text_search::text_search_construct(
file_text_view,
&self.text_search_bar,
&self.find_text_button,
false,
text_search::PanelID::File,
);
let settings = systemd_gui::new_settings();
settings
.bind(
&UNIT_FILE_LINE_NUMBER_ACTION[4..],
&view,
"show-line-numbers",
)
.build();
let (toggle_find_text, open_find_text) =
text_search::create_menu_item(text_search::PanelID::File);
let menu = gio::Menu::new();
let menu_label = pgettext("file", "Display Line Numbers");
let mi = gio::MenuItem::new(Some(&menu_label), Some(UNIT_FILE_LINE_NUMBER_ACTION));
menu.append_item(&mi);
menu.append_item(&toggle_find_text);
menu.append_item(&open_find_text);
let menu_sec = gio::Menu::new();
menu_sec.append_section(None, &menu);
file_text_view.set_extra_menu(Some(&menu_sec));
self.sourceview5_buffer
.set(buffer)
.expect("sourceview5_buffer set once");
self.unit_file_text
.set(view)
.expect("unit_file_text set once");
self.file_dropin_selector.connect_n_toggles_notify(|tg| {
let selected = tg.active();
debug!("selected file {selected}");
});
let unit_file_panel = self.obj().clone();
self.file_dropin_selector.connect_active_notify(move |tg| {
let selected = tg.active();
if selected == GTK_INVALID_LIST_POSITION {
return;
}
debug!("unit file or drop in: {selected}");
unit_file_panel
.imp()
.file_dropin_selector_activate(selected)
});
}
}
impl WidgetImpl for UnitFilePanelImp {}
impl BoxImpl for UnitFilePanelImp {}
fn remove_trailing_newlines(text: &str) -> Result<String, regex::Error> {
let re = Regex::new(r"[\n]+$")?;
Ok(re.replace(text, "\n").to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_grab_index_with_no_suffix() {
let (stem, idx) = UnitFilePanelImp::grab_index("override");
assert_eq!(stem, "override");
assert_eq!(idx, 0);
}
#[test]
fn test_grab_index_with_single_digit() {
let (stem, idx) = UnitFilePanelImp::grab_index("override-1");
assert_eq!(stem, "override");
assert_eq!(idx, 2);
}
#[test]
fn test_grab_index_with_multiple_digits() {
let (stem, idx) = UnitFilePanelImp::grab_index("override-42");
assert_eq!(stem, "override");
assert_eq!(idx, 43);
}
#[test]
fn test_grab_index_with_multiple_hyphens() {
let (stem, idx) = UnitFilePanelImp::grab_index("my-override-5");
assert_eq!(stem, "my-override");
assert_eq!(idx, 6);
}
#[test]
fn test_grab_index_with_trailing_hyphen() {
let (stem, idx) = UnitFilePanelImp::grab_index("override-");
assert_eq!(stem, "override-");
assert_eq!(idx, 0);
}
#[test]
fn test_grab_index_with_non_numeric_suffix() {
let (stem, idx) = UnitFilePanelImp::grab_index("override-abc");
assert_eq!(stem, "override-abc");
assert_eq!(idx, 0);
}
#[test]
fn test_grab_index_with_zero() {
let (stem, idx) = UnitFilePanelImp::grab_index("override-0");
assert_eq!(stem, "override");
assert_eq!(idx, 1);
}
#[test]
fn test_grab_index_with_large_number() {
let (stem, idx) = UnitFilePanelImp::grab_index("override-999");
assert_eq!(stem, "override");
assert_eq!(idx, 1000);
}
#[test]
fn test_file_nav_is_file() {
let fnav = FileNav {
file_path: "/etc/systemd/system/test.service".to_string(),
id: "unit file".to_string(),
status: UnitFileStatus::Edit,
is_drop_in: false,
is_runtime: false,
};
assert!(fnav.is_file());
}
#[test]
fn test_file_nav_is_drop_in() {
let fnav = FileNav {
file_path: "/etc/systemd/system/test.service.d/override.conf".to_string(),
id: "dropin 1".to_string(),
status: UnitFileStatus::Edit,
is_drop_in: true,
is_runtime: false,
};
assert!(!fnav.is_file());
}
#[test]
fn test_file_nav_file_stem_regular_file() {
let fnav = FileNav {
file_path: "/etc/systemd/system/test.service".to_string(),
id: "unit file".to_string(),
status: UnitFileStatus::Edit,
is_drop_in: false,
is_runtime: false,
};
assert_eq!(fnav.file_stem(), Some("test"));
}
#[test]
fn test_file_nav_file_stem_drop_in() {
let fnav = FileNav {
file_path: "/etc/systemd/system/test.service.d/override.conf".to_string(),
id: "dropin 1".to_string(),
status: UnitFileStatus::Edit,
is_drop_in: true,
is_runtime: false,
};
assert_eq!(fnav.file_stem(), Some("override"));
}
#[test]
fn test_file_nav_file_stem_no_extension() {
let fnav = FileNav {
file_path: "/etc/systemd/system/test".to_string(),
id: "test".to_string(),
status: UnitFileStatus::Edit,
is_drop_in: false,
is_runtime: false,
};
assert_eq!(fnav.file_stem(), Some("test"));
}
#[test]
fn test_remove_trailing_newlines() {
let text = "line one\n\nline two\n\n\n";
let cleaned: String = remove_trailing_newlines(text).unwrap();
println!("{cleaned}");
assert_eq!(cleaned, "line one\n\nline two\n");
}
}