use gettextrs::pgettext;
use gtk::{
TemplateChild, gio,
glib::{self},
prelude::*,
subclass::{
box_::BoxImpl,
prelude::*,
widget::{
CompositeTemplateCallbacksClass, CompositeTemplateClass,
CompositeTemplateInitializingExt, WidgetImpl,
},
},
};
use crate::{
consts::SETTING_FIND_IN_TEXT_OPEN,
systemd::{
Dependency,
data::UnitInfo,
enums::{DependencyType, UnitType},
},
systemd_gui,
utils::{font_management::set_text_view_font, text_view_hyperlink::LinkActivator},
widget::{
app_window::AppWindow,
menu_button::{ExMenuButton, OnClose},
text_search::{self, on_new_text},
},
};
use base::enums::UnitDBusLevel;
use std::{
cell::{Cell, RefCell},
collections::{BTreeSet, HashSet},
rc::Rc,
};
use crate::{
utils::{
text_view_hyperlink,
writer::{
HyperLinkType, SPECIAL_GLYPH_TREE_BRANCH, SPECIAL_GLYPH_TREE_RIGHT,
SPECIAL_GLYPH_TREE_SPACE, SPECIAL_GLYPH_TREE_VERTICAL, UnitInfoWriter,
},
},
widget::InterPanelMessage,
};
use strum::IntoEnumIterator;
use tracing::{debug, info, warn};
const PANEL_EMPTY: &str = "empty";
const PANEL_DEPENDENCIES: &str = "dependencies";
const PANEL_SPINNER: &str = "spinner";
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/unit_dependencies_panel.ui")]
pub struct UnitDependenciesPanelImp {
#[template_child]
unit_dependencies_panel_stack: TemplateChild<adw::ViewStack>,
#[template_child]
unit_dependencies_textview: TemplateChild<gtk::TextView>,
#[template_child]
dependency_types_dropdown: TemplateChild<gtk::DropDown>,
#[template_child]
controls_box: TemplateChild<gtk::Box>,
#[template_child]
text_search_bar: TemplateChild<gtk::SearchBar>,
#[template_child]
find_text_button: TemplateChild<gtk::ToggleButton>,
visible_on_page: Cell<bool>,
unit: RefCell<Option<UnitInfo>>,
unit_dependencies_loaded: Cell<bool>,
pub(super) dependency_type: Cell<DependencyType>,
plain: Cell<bool>,
hovering_over_link_tag: Rc<RefCell<Option<gtk::TextTag>>>,
unit_type_filter: RefCell<HashSet<String>>,
}
#[gtk::template_callbacks]
impl UnitDependenciesPanelImp {
#[template_callback]
fn plain_option_toggled(&self, check_button: >k::CheckButton) {
self.plain.set(check_button.is_active());
self.update_dependencies();
}
}
impl UnitDependenciesPanelImp {
pub(crate) fn register(&self, app_window: &AppWindow) {
let activator = LinkActivator::new(Some(app_window.clone()));
text_view_hyperlink::build_textview_link_platform(
&self.unit_dependencies_textview,
self.hovering_over_link_tag.clone(),
activator,
);
}
fn set_visible_on_page(&self, visible: bool) {
debug!("set_visible_on_page val {visible}");
self.visible_on_page.set(visible);
if self.visible_on_page.get()
&& !self.unit_dependencies_loaded.get()
&& self.unit.borrow().is_some()
{
self.update_dependencies()
}
}
fn set_unit(&self, unit: Option<&UnitInfo>) {
let unit = match unit {
Some(u) => u,
None => {
self.unit.replace(None);
self.update_dependencies();
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.update_dependencies()
}
pub(super) fn update_dependencies_filtered(&self, unit_type_filter: &HashSet<String>) {
self.unit_type_filter.replace(unit_type_filter.clone());
self.update_dependencies();
}
pub(super) fn update_dependencies(&self) {
if !self.visible_on_page.get() {
return;
}
let binding = self.unit.borrow();
let Some(unit_ref) = binding.as_ref() else {
info!("No unit file");
self.unit_dependencies_panel_stack
.set_visible_child_name(PANEL_EMPTY);
return;
};
self.unit_dependencies_loaded.set(true);
let dep_type = self.dependency_type.get();
let unit = unit_ref.clone();
let textview = self.unit_dependencies_textview.clone();
let stack = self.unit_dependencies_panel_stack.clone();
let mut plain = self.plain.get();
let unit_type_filter = self.unit_type_filter.borrow().clone();
plain = plain || !unit_type_filter.is_empty();
let level = unit.dbus_level();
let primary_name = unit.primary();
let object_path = unit.object_path();
let text_search_bar = self.text_search_bar.clone();
glib::spawn_future_local(async move {
stack.set_visible_child_name(PANEL_SPINNER);
let dependencies =
gio::spawn_blocking(move || {
match systemd::fetch_unit_dependencies(
level,
&primary_name,
&object_path,
dep_type,
plain,
) {
Ok(dep) => Some(dep),
Err(error) => {
warn!("Fetching {:?} dependencies error {:?}", primary_name, error);
None
}
}
})
.await
.expect("Task needs to finish successfully.");
let Some(mut dependencies) = dependencies else {
stack.set_visible_child_name(PANEL_EMPTY);
return;
};
if !unit_type_filter.is_empty() {
let mut set = BTreeSet::new();
for dep in dependencies.children {
if let Some((_, unit_type)) = dep.unit_name.rsplit_once('.')
&& unit_type_filter.contains(unit_type)
{
set.insert(dep);
}
}
dependencies.children = set;
}
let buf = textview.buffer();
buf.set_text("");
let start_iter = buf.start_iter();
let mut info_writer = UnitInfoWriter::new(buf, start_iter);
info_writer.insertln(&dependencies.unit_name);
let spacer = String::from(SPECIAL_GLYPH_TREE_SPACE);
let mut it = dependencies.children.iter().peekable();
while let Some(child) = it.next() {
UnitDependenciesPanelImp::display_dependencies(
&mut info_writer,
child,
&spacer,
level,
it.peek().is_none(),
);
}
stack.set_visible_child_name(PANEL_DEPENDENCIES);
on_new_text(&text_search_bar);
});
}
fn display_dependencies(
info_writer: &mut UnitInfoWriter,
dependency: &Dependency,
spacer: &str,
level: UnitDBusLevel,
last: bool,
) {
info_writer.insert_state(dependency.state);
info_writer.insert(spacer);
let (glyph, child_pading) = if last {
(SPECIAL_GLYPH_TREE_RIGHT, SPECIAL_GLYPH_TREE_SPACE)
} else {
(SPECIAL_GLYPH_TREE_BRANCH, SPECIAL_GLYPH_TREE_VERTICAL)
};
info_writer.insert(glyph);
info_writer.insert(" ");
info_writer.hyperlink(
&dependency.unit_name,
&dependency.unit_name,
HyperLinkType::Unit(level),
);
info_writer.newline();
let child_spacer = format!("{spacer}{child_pading}");
let mut it = dependency.children.iter().peekable();
while let Some(child) = it.next() {
let child_last = it.peek().is_none();
UnitDependenciesPanelImp::display_dependencies(
info_writer,
child,
&child_spacer,
level,
child_last,
);
}
}
fn setup_dependency_type_dropdown(&self) {
let mut levels_string = Vec::new();
for dep_type in DependencyType::iter() {
levels_string.push(dep_type.label());
}
let level_str: Vec<&str> = levels_string.iter().map(|x| &**x).collect();
let string_list = gtk::StringList::new(&level_str);
self.dependency_types_dropdown.set_model(Some(&string_list));
{
let dependency_panel = self.obj().clone();
self.dependency_types_dropdown
.connect_selected_item_notify(move |dropdown| {
let idx = dropdown.selected();
let dependency_type: DependencyType = idx.into();
debug!(
"System Session Values Selected idx {:?} level {:?}",
idx, -1
);
let old = dependency_panel.replace_dependency_type(dependency_type);
if old != dependency_type {
dependency_panel.update_dependencies()
}
});
}
}
pub(super) fn set_inter_message(&self, action: &InterPanelMessage) {
match *action {
InterPanelMessage::FontProvider(old, new) => {
set_text_view_font(old, new, &self.unit_dependencies_textview)
}
InterPanelMessage::PanelVisible(visible) => self.set_visible_on_page(visible),
InterPanelMessage::UnitChange(unit) => self.set_unit(unit),
InterPanelMessage::IsDark(_) => self.update_dependencies(),
_ => {}
}
}
pub(crate) fn focus_text_search(&self) {
text_search::focus_on_text_entry(&self.text_search_bar)
}
}
#[glib::object_subclass]
impl ObjectSubclass for UnitDependenciesPanelImp {
const NAME: &'static str = "UnitDependenciesPanel";
type Type = super::UnitDependenciesPanel;
type ParentType = gtk::Box;
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 UnitDependenciesPanelImp {
fn constructed(&self) {
self.parent_constructed();
self.unit_dependencies_loaded.set(false);
self.setup_dependency_type_dropdown();
let mut filter_button_unit_type = ExMenuButton::new(
&pgettext("dependency", "Unit Types"),
);
filter_button_unit_type.set_margin_end(5);
filter_button_unit_type.set_tooltip_text(Some(
&pgettext("dependency", "Filter dependencies by types"),
));
for unit_type in UnitType::iter().filter(|x| !matches!(*x, UnitType::Unknown)) {
filter_button_unit_type.add_item(unit_type.as_str());
}
self.controls_box.prepend(&filter_button_unit_type);
let dep = self.obj();
let on_close = OnClose::new_dep(&dep);
filter_button_unit_type.set_on_close(on_close);
text_search::text_search_construct(
&self.unit_dependencies_textview,
&self.text_search_bar,
&self.find_text_button,
true,
text_search::PanelID::Dependencies,
);
let settings = systemd_gui::new_settings();
settings
.bind::<gtk::SearchBar>(
SETTING_FIND_IN_TEXT_OPEN,
&self.text_search_bar,
"search-mode-enabled",
)
.build();
}
}
impl WidgetImpl for UnitDependenciesPanelImp {}
impl BoxImpl for UnitDependenciesPanelImp {}