use adw::subclass::window::AdwWindowImpl;
use gtk::{gio, glib, prelude::*, subclass::prelude::*};
use std::cell::{OnceCell, RefCell};
use std::cmp::Ordering;
use tracing::{debug, error, warn};
use crate::consts::U64MAX;
use crate::systemd;
use crate::systemd::data::UnitInfo;
use crate::systemd_gui::new_settings;
use super::rowitem;
const WINDOW_WIDTH: &str = "unit-properties-window-width";
const WINDOW_HEIGHT: &str = "unit-properties-window-height";
const IS_MAXIMIZED: &str = "unit-properties-is-maximized";
const SEARCH_OPEN: &str = "unit-properties-filter-open";
const FILTER_SHOW_ALL: &str = "unit-properties-fileter-show-all";
const FILTER_TEXT: &str = "unit-properties-filter-text";
#[derive(Debug, Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/unit_properties.ui")]
pub struct InfoWindowImp {
#[template_child]
pub unit_properties: TemplateChild<gtk::ListBox>,
#[template_child]
search_entry: TemplateChild<gtk::SearchEntry>,
#[template_child]
search_bar: TemplateChild<gtk::SearchBar>,
#[template_child]
filter_toggle: TemplateChild<gtk::ToggleButton>,
#[template_child]
show_all_check: TemplateChild<gtk::CheckButton>,
#[template_child]
window_title: TemplateChild<adw::WindowTitle>,
pub(super) store: RefCell<Option<gio::ListStore>>,
last_filter_string: RefCell<String>,
custom_filter: OnceCell<gtk::CustomFilter>,
settings: OnceCell<gio::Settings>,
}
#[gtk::template_callbacks]
impl InfoWindowImp {
#[template_callback]
fn handle_copy_click(&self, _button: >k::Button) {
let clipboard = _button.clipboard();
let unit_prop_store = &self.store;
if let Some(store) = unit_prop_store.borrow().as_ref() {
let n_item = store.n_items();
let mut data = String::new();
for i in 0..n_item {
if let Some(object) = store.item(i)
&& let Ok(x) = object.downcast::<rowitem::Metadata>()
{
data.push_str(&x.unit_prop());
data.push('\t');
data.push_str(&x.prop_value());
data.push('\n')
}
}
clipboard.set_text(&data)
}
}
#[template_callback]
fn search_entry_changed(&self, search_entry: >k::SearchEntry) {
let text = search_entry.text();
debug!("Search text \"{text}\"");
let mut last_filter = self.last_filter_string.borrow_mut();
let change_type = if text.is_empty() {
gtk::FilterChange::LessStrict
} else if text.len() > last_filter.len() && text.contains(last_filter.as_str()) {
gtk::FilterChange::MoreStrict
} else if text.len() < last_filter.len() && last_filter.contains(text.as_str()) {
gtk::FilterChange::LessStrict
} else {
gtk::FilterChange::Different
};
debug!("Current \"{text}\" Prev \"{last_filter}\"");
last_filter.replace_range(.., text.as_str());
if let Some(custom_filter) = self.custom_filter.get() {
custom_filter.changed(change_type);
}
self.set_filter_icon()
}
#[template_callback]
fn show_all_toggle(&self, check: gtk::CheckButton) {
let show_all = check.is_active();
let change_type = if show_all {
gtk::FilterChange::LessStrict
} else {
gtk::FilterChange::MoreStrict
};
if let Some(custom_filter) = self.custom_filter.get() {
custom_filter.changed(change_type);
}
}
}
impl InfoWindowImp {
pub fn fill_data(&self, unit: Option<&UnitInfo>) {
let Some(unit) = unit else {
return;
};
let unit_prop_store = &self.store;
if let Some(ref mut store) = *unit_prop_store.borrow_mut() {
store.remove_all();
match systemd::fetch_system_unit_info_native(unit) {
Ok(mut vec) => {
vec.sort_by(|a, b| {
let c = a.0.cmp(&b.0);
if Ordering::Equal == c {
a.1.cmp(&b.1)
} else {
c
}
});
for (idx, (unit_type, key, value)) in vec.into_iter().enumerate() {
let (value, empty) = convert_to_string(&value);
let data = rowitem::Metadata::new(idx as u32, unit_type, key, value, empty);
store.append(&data);
}
}
Err(e) => warn!("Fails to retreive Unit info: {e:?}"),
}
} else {
warn!("Store not supposed to be None");
};
self.window_title.set_subtitle(&unit.primary());
}
pub fn fill_systemd_info(&self) {
let unit_prop_store = &self.store;
if let Some(ref mut store) = *unit_prop_store.borrow_mut() {
store.remove_all();
match systemd::fetch_system_info() {
Ok(map) => {
for (idx, (unit_type, key, value)) in map.into_iter().enumerate() {
let data = rowitem::Metadata::new(idx as u32, unit_type, key, value, false);
store.append(&data);
}
}
Err(e) => error!("Fail to retreive Unit info: {e:?}"),
}
} else {
warn!("Store not supposed to be None");
};
self.obj().set_title(Some("Systemd Info"));
}
fn create_filter(&self) -> gtk::CustomFilter {
let search_entry = self.search_entry.clone();
let show_all_check = self.show_all_check.clone();
gtk::CustomFilter::new(move |object| {
let Some(meta) = object.downcast_ref::<rowitem::Metadata>() else {
error!("some wrong downcast_ref {object:?}");
return false;
};
let show_all = show_all_check.is_active();
if !show_all && meta.is_empty() {
return false;
}
let text = search_entry.text();
if text.is_empty() {
return true;
}
let texts = text.as_str();
if text.chars().any(|c| c.is_ascii_uppercase()) {
meta.unit_prop().contains(texts) || meta.prop_value().contains(texts)
} else {
meta.unit_prop().to_ascii_lowercase().contains(texts)
|| meta.prop_value().to_ascii_lowercase().contains(texts)
}
})
}
fn settings(&self) -> &gio::Settings {
match self.settings.get() {
Some(settings) => settings,
None => {
let settings: gio::Settings = new_settings();
self.settings
.set(settings)
.expect("`settings` should not be set before calling `setup_settings`.");
self.settings.get().expect("`settings` should be set ")
}
}
}
fn load_window_size(&self) {
let settings = self.settings();
let mut width = settings.int(WINDOW_WIDTH);
let mut height = settings.int(WINDOW_HEIGHT);
let is_maximized = settings.boolean(IS_MAXIMIZED);
let obj = self.obj();
let (def_width, def_height) = obj.default_size();
if width <= 0 {
width = def_width;
if width <= 0 {
width = 650;
}
}
if height <= 0 {
height = def_height;
if height <= 0 {
height = 600;
}
}
obj.set_default_size(width, height);
if is_maximized {
obj.maximize();
}
let search_open = settings.boolean(SEARCH_OPEN);
self.filter_toggle.set_active(search_open);
let show_all = settings.boolean(FILTER_SHOW_ALL);
self.show_all_check.set_active(show_all);
let filter_text = settings.string(FILTER_TEXT);
self.search_entry.set_text(&filter_text);
}
pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
let obj = self.obj();
let (width, height) = obj.default_size();
let settings = self.settings();
settings.set_int(WINDOW_WIDTH, width)?;
settings.set_int(WINDOW_HEIGHT, height)?;
settings.set_boolean(IS_MAXIMIZED, obj.is_maximized())?;
let search_open = self.filter_toggle.is_active();
let show_all = self.show_all_check.is_active();
let filter_text = self.search_entry.text();
settings.set_boolean(SEARCH_OPEN, search_open)?;
settings.set_boolean(FILTER_SHOW_ALL, show_all)?;
settings.set_string(FILTER_TEXT, &filter_text)?;
Ok(())
}
fn set_filter_icon(&self) {
let icon = if self.search_entry.text().is_empty() {
"funnel-outline-symbolic"
} else {
"funnel-symbolic"
};
self.filter_toggle.set_icon_name(icon);
}
}
#[glib::object_subclass]
impl ObjectSubclass for InfoWindowImp {
const NAME: &'static str = "UNIT_PROPERTIES_DIALOG";
type Type = super::InfoWindow;
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();
}
}
const WIDTH_CHAR_SIZE: usize = 36;
impl ObjectImpl for InfoWindowImp {
fn constructed(&self) {
self.parent_constructed();
let unit_prop_store = gio::ListStore::new::<rowitem::Metadata>();
let no_selection = gtk::NoSelection::new(Some(unit_prop_store.clone()));
let filter = self.create_filter();
self.custom_filter
.set(filter.clone())
.expect("custom filter set once");
let filtering_model = gtk::FilterListModel::new(Some(no_selection), Some(filter));
self.store.replace(Some(unit_prop_store));
self.search_bar
.bind_property("search-mode-enabled", &self.filter_toggle.clone(), "active")
.bidirectional()
.build();
self.search_entry.set_width_chars(WIDTH_CHAR_SIZE as i32);
self.load_window_size();
self.unit_properties
.bind_model(Some(&filtering_model), |object| {
let meta = match object.downcast_ref::<rowitem::Metadata>() {
Some(any_objet) => any_objet,
None => {
error!("No linked object");
let list_box_row = gtk::ListBoxRow::new();
return list_box_row.upcast::<gtk::Widget>();
}
};
let box_ = gtk::Box::new(gtk::Orientation::Horizontal, 15);
let mut long_text = false;
let unit_prop_value = meta.unit_prop();
let key_label = if unit_prop_value.chars().count() > WIDTH_CHAR_SIZE {
long_text = true;
let mut tmp = String::new();
tmp.push_str(&unit_prop_value[..(WIDTH_CHAR_SIZE - 3)]);
tmp.push_str("...");
tmp
} else {
unit_prop_value
};
let unit_type = meta.unit_type().as_str();
let l1 = gtk::Label::builder()
.label(key_label)
.width_chars(WIDTH_CHAR_SIZE as i32)
.xalign(0.0)
.max_width_chars(30)
.single_line_mode(true)
.selectable(true)
.build();
if long_text {
l1.set_tooltip_text(Some(&meta.unit_prop()));
}
let l2 = gtk::Label::builder()
.label(meta.prop_value())
.selectable(true)
.build();
let idx = meta.index().to_string();
let l0 = gtk::Label::builder()
.label(idx)
.width_chars(3)
.selectable(false)
.css_classes(["idx"])
.build();
let lt = gtk::Label::builder()
.label(unit_type)
.width_chars(10)
.xalign(0.0)
.single_line_mode(true)
.selectable(true)
.build();
box_.append(&l0);
box_.append(<);
box_.append(&l1);
box_.append(&l2);
box_.upcast::<gtk::Widget>()
});
}
}
impl WidgetImpl for InfoWindowImp {}
impl WindowImpl for InfoWindowImp {
fn close_request(&self) -> glib::Propagation {
debug!("Close window");
if let Err(_err) = self.save_window_size() {
error!("Failed to save window state");
}
self.parent_close_request();
glib::Propagation::Proceed
}
}
impl AdwWindowImpl for InfoWindowImp {}
fn convert_to_string(value: &zvariant::Value) -> (String, bool) {
match value {
zvariant::Value::U8(i) => (i.to_string(), false),
zvariant::Value::Bool(b) => (b.to_string(), false),
zvariant::Value::I16(i) => (i.to_string(), false),
zvariant::Value::U16(i) => (i.to_string(), *i == u16::MAX),
zvariant::Value::I32(i) => (i.to_string(), false),
zvariant::Value::U32(i) => (i.to_string(), *i == u32::MAX),
zvariant::Value::I64(i) => (i.to_string(), false),
zvariant::Value::U64(i) => (i.to_string(), *i == U64MAX),
zvariant::Value::F64(i) => (i.to_string(), false),
zvariant::Value::Str(s) => {
let s = s.to_string();
let empty = s.is_empty();
(s, empty)
}
zvariant::Value::Signature(s) => (s.to_string(), false),
zvariant::Value::ObjectPath(op) => {
let s = op.to_string();
let empty = s.is_empty();
(s, empty)
}
zvariant::Value::Value(v) => {
let s = v.to_string();
let empty = s.is_empty();
(s, empty)
}
zvariant::Value::Array(a) => {
if a.is_empty() {
("[]".to_owned(), true)
} else {
let mut d_str = String::from("[ ");
let mut is_empty = false;
let mut it = a.iter().peekable();
while let Some(mi) = it.next() {
let (sub_value, sub_empty) = convert_to_string(mi);
is_empty |= sub_empty;
d_str.push_str(&sub_value);
if it.peek().is_some() {
d_str.push_str(", ");
}
}
d_str.push_str(" ]");
(d_str, is_empty)
}
}
zvariant::Value::Dict(d) => {
let mut d_str = String::from("{ ");
for (mik, miv) in d.iter() {
d_str.push_str(&convert_to_string(mik).0);
d_str.push_str(" : ");
d_str.push_str(&convert_to_string(miv).0);
}
d_str.push_str(" }");
(d_str, false)
}
zvariant::Value::Structure(stc) => {
let mut d_str = String::from("{ ");
let mut it = stc.fields().iter().peekable();
let mut is_empty = false;
while let Some(mi) = it.next() {
let (sub_value, sub_empty) = convert_to_string(mi);
is_empty |= sub_empty;
d_str.push_str(&sub_value);
if it.peek().is_some() {
d_str.push_str(", ");
}
}
d_str.push_str(" }");
(d_str, is_empty)
}
zvariant::Value::Fd(fd) => (fd.to_string(), false),
}
}