mod column_factories;
#[macro_use]
mod construct;
mod favorites;
pub mod pop_menu;
use crate::{
consts::{
ACTION_UNIT_LIST_FILTER, ACTION_UNIT_LIST_FILTER_CLEAR, ACTION_WIN_FAVORITE_SET,
ACTION_WIN_FAVORITE_TOGGLE, ACTION_WIN_HIDE_UNIT_COL, ACTION_WIN_REFRESH_POP_MENU,
ACTION_WIN_REFRESH_UNIT_LIST, ALL_FILTER_KEY, FILTER_MARK,
KEY_PREF_UNIT_LIST_DISPLAY_SUMMARY, WIN_ACTION_INCLUDE_UNIT_FILES,
},
systemd::{
ListUnitResponse, UnitProperties, UnitPropertiesFlags,
data::{UnitInfo, UnitPropertySetter},
enums::{LoadState, UnitFileStatus, UnitType},
socket_unit::SocketUnitInfo,
},
systemd_gui, upgrade,
widget::{
InterPanelMessage,
app_window::AppWindow,
control_action_dialog::imp::complete_unit_information,
preferences::data::{
DbusLevel, KEY_PREF_CASE_INSENSITIVE_DEFAULT, KEY_PREF_UNIT_LIST_DISPLAY_COLORS,
PREFERENCES,
},
unit_list::{
COL_ID_UNIT, UnitCuratedList, UnitListPanel,
column::SysdColumn,
filter::{
UnitListFilterWindow, custom_bool, custom_num, custom_str, filter_active_state,
filter_bus_level, filter_enable_status, filter_load_state, filter_preset,
filter_sub_state, filter_unit_description, filter_unit_name, filter_unit_type,
unit_prop_filter::{
FilterBool, FilterElement, FilterNum, FilterText, UnitPropertyAssessor,
UnitPropertyFilter, UnitPropertyFilterType,
},
},
get_clean_col_title,
imp::{construct::construct_column_view, favorites::save_favorites},
search_controls::UnitListSearchControls,
},
unit_properties_selector::{
data_selection::UnitPropertySelection,
save::{self},
},
},
};
use base::enums::UnitDBusLevel;
use flagset::FlagSet;
use gtk::{
Adjustment, TemplateChild,
gio::{self, glib::VariantTy},
glib::{self, Properties, WeakRef},
prelude::*,
subclass::{
box_::BoxImpl,
prelude::*,
widget::{
CompositeTemplateCallbacksClass, CompositeTemplateClass,
CompositeTemplateInitializingExt, WidgetImpl,
},
},
};
use std::{
borrow::Cow,
cell::{Cell, OnceCell, Ref, RefCell, RefMut},
collections::{HashMap, HashSet, hash_map},
hash::{Hash, Hasher},
rc::Rc,
sync::OnceLock,
time::Duration,
};
use systemd::{SystemdSignal, errors::SystemdErrors, init_signal_watcher, runtime};
use tokio::{
sync::{broadcast::Receiver, mpsc},
task::AbortHandle,
};
use tokio_stream::StreamExt;
use tracing::{debug, error, info, trace, warn};
static SOCKET_LISTEN_QUARK: OnceLock<glib::Quark> = OnceLock::new();
static PATH_QUARK: OnceLock<glib::Quark> = OnceLock::new();
const UNIT_LIST_VIEW_PAGE: &str = "unit_list";
const RESTRICTIVE_FILTER_VIEW_PAGE: &str = "restrictive_filter";
#[derive(Debug, Clone)]
struct UnitKey {
level: UnitDBusLevel,
primary: String,
update_properties: Cell<UnitProperties>,
}
impl UnitKey {
fn new(unit: &UnitInfo) -> Self {
Self::new_string(unit.dbus_level(), unit.primary())
}
fn new_string(level: UnitDBusLevel, primary: String) -> Self {
let f = FlagSet::<UnitPropertiesFlags>::empty();
Self::new_string_flags(level, primary, f)
}
fn new_string_flags(
level: UnitDBusLevel,
primary: String,
flags: impl Into<FlagSet<UnitPropertiesFlags>>,
) -> Self {
UnitKey {
level,
primary,
update_properties: Cell::new(UnitProperties(flags.into())),
}
}
fn intersec(&self, flags: impl Into<FlagSet<UnitPropertiesFlags>>) {
let f = self.update_properties.get().0 & flags;
self.update_properties.set(UnitProperties(f));
}
}
impl PartialEq for UnitKey {
fn eq(&self, other: &UnitKey) -> bool {
self.level == other.level && self.primary == other.primary
}
}
impl Eq for UnitKey {}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct UnitKeyRef<'a> {
level: UnitDBusLevel,
primary: &'a str,
}
trait UnitKeyInterface {
fn as_key_ref(&self) -> UnitKeyRef<'_>;
}
impl UnitKeyInterface for UnitKey {
fn as_key_ref(&self) -> UnitKeyRef<'_> {
UnitKeyRef {
level: self.level,
primary: self.primary.as_str(),
}
}
}
impl<'a> UnitKeyInterface for UnitKeyRef<'a> {
fn as_key_ref(&self) -> UnitKeyRef<'_> {
*self
}
}
impl<'a> PartialEq for dyn UnitKeyInterface + 'a {
fn eq(&self, other: &Self) -> bool {
self.as_key_ref() == other.as_key_ref()
}
}
impl<'a> Eq for dyn UnitKeyInterface + 'a {}
impl<'a> Hash for dyn UnitKeyInterface + 'a {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_key_ref().hash(state);
}
}
impl Hash for UnitKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_key_ref().hash(state);
}
}
impl<'a> std::borrow::Borrow<dyn UnitKeyInterface + 'a> for UnitKey {
fn borrow(&self) -> &(dyn UnitKeyInterface + 'a) {
self
}
}
impl<'a> UnitKeyRef<'a> {
fn new(level: UnitDBusLevel, primary: &'a str) -> Self {
UnitKeyRef { level, primary }
}
fn key_owned(self, flags: impl Into<FlagSet<UnitPropertiesFlags>>) -> UnitKey {
UnitKey::new_string_flags(self.level, self.primary.to_owned(), flags)
}
}
impl<'a> PartialEq<UnitKey> for UnitKeyRef<'a> {
fn eq(&self, other: &UnitKey) -> bool {
self.level == other.level && self.primary == other.primary
}
}
impl<'a> PartialEq<UnitKeyRef<'a>> for UnitKey {
fn eq(&self, other: &UnitKeyRef<'a>) -> bool {
self.level == other.level && self.primary == other.primary
}
}
type UnitPropertyFiltersContainer =
RefCell<HashMap<String, Rc<RefCell<Box<dyn UnitPropertyFilter>>>>>;
type AppliedUnitPropertyFilters = OnceCell<Rc<RefCell<Vec<Box<dyn UnitPropertyAssessor>>>>>;
#[derive(Default, gtk::CompositeTemplate, Properties)]
#[template(resource = "/io/github/plrigaux/sysd-manager/unit_list_panel.ui")]
#[properties(wrapper_type = super::UnitListPanel)]
pub struct UnitListPanelImp {
list_store: OnceCell<gio::ListStore>,
units_map: Rc<RefCell<HashMap<UnitKey, UnitInfo>>>,
favorites: RefCell<HashMap<UnitKey, Option<UnitInfo>>>,
unit_list_sort_list_model: RefCell<gtk::SortListModel>,
units_browser: OnceCell<gtk::ColumnView>,
single_selection: OnceCell<gtk::SingleSelection>,
#[template_child]
search_bar: TemplateChild<gtk::SearchBar>,
filter_list_model: RefCell<gtk::FilterListModel>,
#[template_child]
panel_stack: TemplateChild<adw::ViewStack>,
#[template_child]
scrolled_window: TemplateChild<gtk::ScrolledWindow>,
#[template_child]
summary: TemplateChild<gtk::Box>,
#[template_child]
unit_files_count_label: TemplateChild<gtk::Label>,
#[template_child]
loaded_units_count_label: TemplateChild<gtk::Label>,
#[template_child]
unit_filtered_count_label: TemplateChild<gtk::Label>,
#[property(get, set=Self::set_unit_files_count)]
unit_files_count: Cell<u32>,
#[property(get, set=Self::set_loaded_units_count)]
loaded_units_count: Cell<u32>,
#[property(get, set=Self::set_unit_filter_count)]
unit_filtered_count: Cell<u32>,
#[property(get, set=Self::set_case_incensitive_default)]
case_incensitive_default: Cell<bool>,
#[property(get, set, default)]
selected_list_view: Cell<UnitCuratedList>,
#[property(get, set)]
include_unit_files: Cell<bool>,
search_controls: OnceCell<UnitListSearchControls>,
refresh_unit_list_button: WeakRef<gtk::Button>,
unit: RefCell<Option<UnitInfo>>,
pub force_selected_index: Cell<Option<u32>>,
#[property(name = "display-color", get, set)]
pub display_color: Cell<bool>,
pub unit_property_filters: UnitPropertyFiltersContainer,
pub applied_unit_property_filters: AppliedUnitPropertyFilters,
app_window: OnceCell<AppWindow>,
current_column_view_column_definition_list: RefCell<Vec<UnitPropertySelection>>,
default_column_view_column_definition_list: OnceCell<Vec<UnitPropertySelection>>,
abort_handles: RefCell<Vec<AbortHandle>>,
pop_menu: OnceCell<pop_menu::UnitPopMenu>,
}
macro_rules! update_search_entry {
($self:expr, $id:expr, $update_widget:expr, $text:expr) => {{
if $update_widget && $id == COL_ID_UNIT {
$self.search_entry_set_text($text);
}
}};
}
macro_rules! single_selection {
($self:expr) => {
$self.single_selection.get().unwrap()
};
}
macro_rules! units_browser {
($self:expr) => {
$self.units_browser.get().unwrap()
};
}
#[gtk::template_callbacks]
impl UnitListPanelImp {
#[template_callback]
fn sections_changed(&self, position: u32) {
info!("sections_changed {position}");
}
#[template_callback]
fn legend_button_clicked(&self, _button: gtk::Button) {
self.summary.set_visible(false);
}
}
impl UnitListPanelImp {
pub(super) fn register_selection_change(
&self,
app_window: &AppWindow,
refresh_unit_list_button: >k::Button,
) {
self.app_window
.set(app_window.clone())
.expect("app_window set once");
let app_window_clone = app_window.clone();
let unit_list = self.obj().clone();
single_selection!(self).connect_selected_item_notify(move |single_selection| {
info!(
"connect_selected_notify idx {}",
single_selection.selected()
);
let Some(object) = single_selection.selected_item() else {
warn!("No unit selected");
return;
};
let Some(unit) = object.downcast_ref::<UnitInfo>() else {
error!("Object.downcast::<UnitInfo>");
return;
};
info!("Selection changed, new unit {}", unit.primary());
unit_list.imp().set_unit_internal(unit);
app_window_clone.selection_change(Some(unit));
});
self.refresh_unit_list_button
.set(Some(refresh_unit_list_button));
let action_entry = {
let units_browser = units_browser!(self).clone();
gio::ActionEntry::builder(&ACTION_WIN_HIDE_UNIT_COL[4..])
.activate(move |_application: &AppWindow, _b, target_value| {
if let Some(value) = target_value {
let key = Some(value.get::<String>().expect("variant always be String"));
let columns_list_model = units_browser.columns();
if let Some(cur_column) = columns_list_model
.iter::<gtk::ColumnViewColumn>()
.filter_map(|item| item.ok())
.find(|col| col.id().map(|s| s.to_string()) == key)
{
cur_column.set_visible(false);
}
}
})
.parameter_type(Some(VariantTy::STRING))
.build()
};
let list_filter_action_entry = {
let unit_list_panel = self.obj().clone();
gio::ActionEntry::builder(ACTION_UNIT_LIST_FILTER)
.activate(move |_application: &AppWindow, _b, target_value| {
let column_id = target_value
.map(|var| var.get::<String>().expect("variant always be String"));
debug!("Filter list, col {column_id:?}");
let filter_win = UnitListFilterWindow::new(column_id, &unit_list_panel);
filter_win.construct_filter_dialog();
filter_win.present();
})
.parameter_type(Some(VariantTy::STRING))
.build()
};
let list_filter_action_entry_blank = {
let unit_list_panel = self.obj().clone();
gio::ActionEntry::builder("unit_list_filter_blank")
.activate(move |_application: &AppWindow, _b, _target_value| {
let filter_win = UnitListFilterWindow::new(None, &unit_list_panel);
filter_win.construct_filter_dialog();
filter_win.present();
})
.build()
};
let list_filter_clear_action_entry = {
let unit_list_panel = self.obj().clone();
gio::ActionEntry::builder(ACTION_UNIT_LIST_FILTER_CLEAR)
.activate(move |_application: &AppWindow, _b, target_value| {
if let Some(v) = target_value
&& let Some(filter_key) = v.get::<String>()
{
unit_list_panel.imp().clear_filters(&filter_key);
}
})
.parameter_type(Some(VariantTy::STRING))
.build()
};
let refresh_unit_list = {
let unit_list_panel = self.obj().clone();
gio::ActionEntry::builder(&ACTION_WIN_REFRESH_UNIT_LIST[4..])
.activate(move |_application: &AppWindow, _, _| {
info!("Action refresh called");
unit_list_panel.imp().fill_store(None);
})
.build()
};
let refresh_pop_menu = {
let unit_list_panel = self.obj().clone();
gio::ActionEntry::builder(&ACTION_WIN_REFRESH_POP_MENU[4..])
.activate(move |_application: &AppWindow, _, _| {
debug!("Action refresh pop menu called");
let Some(pop_menu) = unit_list_panel.imp().pop_menu.get() else {
error!("No Pop Menu");
return;
};
pop_menu.refresh_buttons_style();
})
.build()
};
let set_favorite = {
let unit_list_panel = self.obj().clone();
gio::ActionEntry::builder(&ACTION_WIN_FAVORITE_TOGGLE[4..])
.activate(move |_application: &AppWindow, _, _| {
info!("Action Toggle Favorite");
unit_list_panel.imp().toggle_unit_favorite();
})
.build()
};
app_window.add_action_entries([
action_entry,
list_filter_action_entry,
list_filter_action_entry_blank,
list_filter_clear_action_entry,
refresh_unit_list,
refresh_pop_menu,
set_favorite,
]);
let settings = systemd_gui::new_settings();
let action = settings.create_action(UnitCuratedList::base_action());
app_window.add_action(&action);
let unit_list_panel = self.obj().clone();
let unit_list_panel2 = self.obj().clone();
settings
.bind::<UnitListPanel>(
UnitCuratedList::base_action(),
&self.obj(),
"selected-list-view",
)
.mapping(move |variant, _| {
let unit_list_view: UnitCuratedList = variant.into();
unit_list_panel.imp().fill_store(Some(unit_list_view));
Some(unit_list_view.to_value())
})
.set_mapping(move |value, _| {
let unit_list_view = value
.get::<UnitCuratedList>()
.inspect_err(|err| warn!("Conv error {:?}", err))
.unwrap_or(UnitCuratedList::Defaut);
unit_list_panel2.imp().fill_store(Some(unit_list_view));
Some(unit_list_view.id().to_variant())
})
.build();
let action = settings.create_action(&WIN_ACTION_INCLUDE_UNIT_FILES[4..]);
let unit_list_panel = self.obj().clone();
action.connect_state_notify(move |_| {
let view = unit_list_panel.selected_list_view();
info!("include unit file {:?}", view);
if matches!(
view,
UnitCuratedList::Timers
| UnitCuratedList::Path
| UnitCuratedList::Automount
| UnitCuratedList::Sockets
| UnitCuratedList::Services
) {
let unit_list_panel = unit_list_panel.clone();
glib::spawn_future_local(async move {
unit_list_panel.imp().fill_store(None);
});
}
});
app_window.add_action(&action);
settings
.bind::<UnitListPanel>(
&WIN_ACTION_INCLUDE_UNIT_FILES[4..],
&self.obj(),
&WIN_ACTION_INCLUDE_UNIT_FILES[4..],
)
.build();
let action = settings.create_action(&KEY_PREF_UNIT_LIST_DISPLAY_SUMMARY[4..]);
app_window.add_action(&action);
}
fn generate_column_list(&self) -> Vec<gtk::ColumnViewColumn> {
let list_model: gio::ListModel = units_browser!(self).columns();
let mut col_list = Vec::with_capacity(list_model.n_items() as usize);
for column_view_column in list_model
.iter::<gtk::ColumnViewColumn>()
.filter_map(|item| {
item.inspect_err(|err| error!("Expect gtk::ColumnViewColumn> {err:?}"))
.ok()
})
{
col_list.push(column_view_column);
}
col_list
}
fn fill_store(&self, new_view: Option<UnitCuratedList>) {
if let Some(new_view) = new_view {
self.save_config();
self.obj().set_selected_list_view(new_view);
}
let view = self.selected_list_view.get();
debug!("fill store {:?}", view);
let include_unit_files = self.include_unit_files.get();
let cols = construct_column_view(self.display_color.get(), view, include_unit_files);
self.set_new_columns(cols, false);
self.fill_browser();
}
fn fill_browser(&self) {
let list_store = self.list_store.get().expect("LIST STORE NOT NONE").clone();
let main_unit_map_rc: Rc<RefCell<HashMap<UnitKey, UnitInfo>>> = self.units_map.clone();
let panel_stack = self.panel_stack.clone();
let single_selection = single_selection!(self).clone();
let unit_list = self.obj().clone();
let units_browser = units_browser!(self).clone();
let view = self.selected_list_view.get();
let dbus_level = PREFERENCES.dbus_level();
let refresh_unit_list_button = upgrade!(self.refresh_unit_list_button);
self.unit_list_sort_list_model
.borrow()
.set_sorter(None::<>k::Sorter>);
self.aborts_handles();
glib::spawn_future_local(async move {
refresh_unit_list_button.set_sensitive(false);
panel_stack.set_visible_child_name("spinner");
let Ok(retrieved_units) = unit_list
.imp()
.retrieve_unit_list(dbus_level, view, &unit_list)
.await
else {
panel_stack.set_visible_child_name("error");
return;
};
let (loaded_count, file_count) = retrieved_units.iter().fold((0, 0), |a, b| {
let l = b.r_len();
(a.0 + l.0, a.1 + l.1)
});
unit_list.set_loaded_units_count(loaded_count as u32);
unit_list.set_unit_files_count(file_count as u32);
let n_items = list_store.n_items();
list_store.remove_all();
let total = retrieved_units.iter().fold(0, |acc, i| acc + i.t_len());
#[allow(clippy::mutable_key_type)]
let mut all_units: HashMap<UnitKey, UnitInfo> = HashMap::with_capacity(total);
for (system_unit_file, flags) in retrieved_units.into_iter().map(|s| {
let f = s.update_flags();
(s, f)
}) {
match system_unit_file {
ListUnitResponse::Loaded(level, lunits) => {
for loaded_unit in lunits.into_iter() {
let key = UnitKeyRef::new(level, &loaded_unit.primary_unit_name);
if let Some((key, unit)) =
all_units.get_key_value(&key as &dyn UnitKeyInterface)
{
key.intersec(flags);
unit.update_from_loaded_unit(loaded_unit);
} else {
let key = key.key_owned(flags);
let unit = UnitInfo::from_listed_unit(loaded_unit, level);
list_store.append(&unit);
all_units.insert(key, unit);
}
}
}
ListUnitResponse::File(level, items) => {
for unit_file in items {
let key = UnitKeyRef::new(level, unit_file.unit_primary_name());
if let Some((key, unit)) =
all_units.get_key_value(&key as &dyn UnitKeyInterface)
{
key.intersec(flags);
unit.update_from_unit_file(unit_file);
} else {
let key = key.key_owned(flags);
let unit = UnitInfo::from_unit_file(unit_file, level);
list_store.append(&unit);
all_units.insert(key, unit);
}
}
}
};
}
if view == UnitCuratedList::Favorites {
for (key, _) in unit_list.imp().favorites.borrow().iter() {
if !all_units.contains_key(key) {
let unit = UnitInfo::from_unit_key(&key.primary, key.level);
list_store.append(&unit);
all_units.insert(key.clone(), unit);
}
}
}
main_unit_map_rc.replace(all_units);
let sort_func = column_sorter_lambda!(primary, dbus_level);
list_store.sort(sort_func);
info!("Unit list refreshed! list size {}", list_store.n_items());
let mut force_selected_index = gtk::INVALID_LIST_POSITION;
let selected_unit = unit_list.imp().selected_unit();
if let Some(selected_unit) = selected_unit {
let selected_unit_name = selected_unit.primary();
debug!(
"LS items-n {} name {}",
list_store.n_items(),
selected_unit_name
);
if let Some(index) = list_store.find_with_equal_func(|object| {
let unit = object
.downcast_ref::<UnitInfo>()
.expect("Needs to be UnitInfo");
unit.primary().eq(&selected_unit_name)
}) {
info!(
"Force selection to index {index:?} to select unit {selected_unit_name:?}"
);
single_selection.select_item(index, true);
force_selected_index = index;
}
}
unit_list
.imp()
.force_selected_index
.set(Some(force_selected_index));
refresh_unit_list_button.set_sensitive(true);
if n_items > 0 {
focus_on_row(&unit_list, &units_browser);
}
panel_stack.set_visible_child_name(UNIT_LIST_VIEW_PAGE);
unit_list.imp().fetch_custom_unit_properties();
});
}
pub(super) fn button_search_toggled(&self, toggle_button_is_active: bool) {
self.search_bar.set_search_mode(toggle_button_is_active);
if toggle_button_is_active {
let Some(s_controls) = self.search_controls.get() else {
error!("search_controls not initialized");
return;
};
s_controls.grab_focus_on_search_entry();
let applied_assessors = self
.applied_unit_property_filters
.get()
.expect("applied_assessors not null")
.borrow();
s_controls.set_filter_is_set(!applied_assessors.is_empty());
}
}
pub fn set_unit_internal(&self, unit: &UnitInfo) {
let _ = self.unit.replace(Some(unit.clone()));
self.set_unit_favorite(Some(unit));
}
pub fn set_unit(&self, unit: Option<&UnitInfo>) -> Option<UnitInfo> {
let Some(unit) = unit else {
self.unit.replace(None);
self.set_unit_favorite(None);
return None;
};
self.set_unit_favorite(Some(unit));
let old = self.unit.replace(Some(unit.clone()));
if let Some(old) = old
&& old.primary() == unit.primary()
{
info!("List {} == {}", old.primary(), unit.primary());
return Some(old);
}
let unit_name = unit.primary();
info!(
"Unit List {} list_store {} filter {} sort_model {}",
unit_name,
self.list_store
.get()
.map(|ls| ls.n_items())
.unwrap_or_default(),
self.filter_list_model.borrow().n_items(),
self.unit_list_sort_list_model.borrow().n_items()
);
if let Some(unit2) = self.units_map.borrow().get(&UnitKey::new(unit)) {
self.unit.replace(Some(unit2.clone()));
} else {
self.add_one_unit(unit, true);
}
if let Some(filter) = self.filter_list_model.borrow().filter()
&& !filter.match_(unit)
{
single_selection!(self).set_selected(gtk::INVALID_LIST_POSITION);
info!("Unit {unit_name} no Match");
return Some(unit.clone());
}
let finding = self
.list_store
.get()
.expect("LIST STORE NOT NONE")
.find_with_equal_func(|object| {
let Some(unit_item) = object.downcast_ref::<UnitInfo>() else {
error!("item.downcast_ref::<UnitBinding>()");
return false;
};
unit_name == unit_item.primary()
});
if let Some(row) = finding {
info!("Scroll to row {row}");
units_browser!(self).scroll_to(
row, None,
gtk::ListScrollFlags::FOCUS | gtk::ListScrollFlags::SELECT,
None,
);
}
Some(unit.clone())
}
fn set_unit_favorite(&self, unit: Option<&UnitInfo>) {
let Some(win) = self.app_window.get() else {
return;
};
let fav = if let Some(unit) = unit {
let key = UnitKey::new(unit);
self.favorites.borrow().contains_key(&key)
} else {
false
};
win.change_action_state(&ACTION_WIN_FAVORITE_SET[4..], &fav.to_variant());
}
fn toggle_unit_favorite(&self) {
let Some(win) = self.app_window.get() else {
return;
};
let Some(unit) = self.unit.borrow().clone() else {
warn!("No unit selected");
return;
};
let fav_len = self.favorites.borrow().len();
let key = UnitKey::new(&unit);
let fav = if self.favorites.borrow().contains_key(&key) {
self.favorites.borrow_mut().remove(&key);
false
} else {
self.favorites.borrow_mut().insert(key, Some(unit));
true
};
if fav_len != self.favorites.borrow().len() {
let ref_map = self.favorites.borrow();
let favorites: Vec<_> = ref_map.keys().collect();
save_favorites(&favorites);
}
debug!("is unit favorite {fav}");
win.change_action_state(&ACTION_WIN_FAVORITE_SET[4..], &fav.to_variant());
if let Some(pop) = self.pop_menu.get() {
pop.set_favorite(fav);
}
}
fn manage_unit_new_remove(&self, signal: SystemdSignal) {
let Some(unit) = signal.create_unit() else {
return;
};
self.add_one_unit(&unit, matches!(signal, SystemdSignal::UnitNew(_, _)));
}
fn can_add_unit_to_view(&self, view: UnitCuratedList, unit: &UnitInfo) -> bool {
match view {
UnitCuratedList::Defaut => Self::match_level(unit),
UnitCuratedList::LoadedUnit => Self::match_level(unit),
UnitCuratedList::UnitFiles => Self::match_level(unit),
UnitCuratedList::Services => {
unit.unit_type() == UnitType::Service && Self::match_level(unit)
}
UnitCuratedList::Timers => {
unit.unit_type() == UnitType::Timer && Self::match_level(unit)
}
UnitCuratedList::Sockets => {
unit.unit_type() == UnitType::Socket && Self::match_level(unit)
}
UnitCuratedList::Path => unit.unit_type() == UnitType::Path && Self::match_level(unit),
UnitCuratedList::Automount => {
unit.unit_type() == UnitType::Automount && Self::match_level(unit)
}
UnitCuratedList::Custom => Self::match_level(unit),
UnitCuratedList::Favorites => false,
}
}
fn match_level(unit: &UnitInfo) -> bool {
let screen_level = PREFERENCES.dbus_level();
let unit_level = unit.dbus_level();
match (screen_level, unit_level) {
(DbusLevel::UserSession, UnitDBusLevel::System) => false,
(DbusLevel::UserSession, _) => true,
(DbusLevel::System, UnitDBusLevel::UserSession) => false,
(DbusLevel::System, _) => true,
(DbusLevel::SystemAndSession, _) => true,
}
}
fn add_one_unit(&self, unit: &UnitInfo, add_or_remove: bool) {
let view = self.selected_list_view.get();
if !self.can_add_unit_to_view(view, unit) {
return;
}
let panel = self.obj().clone();
let unit = unit.clone();
glib::spawn_future_local(async move {
if add_or_remove {
let _result = complete_unit_information(&unit).await;
let key = UnitKey::new(&unit);
let mut unit_map = panel.imp().units_map.borrow_mut();
if let hash_map::Entry::Vacant(vacant_entry) = unit_map.entry(key) {
let Some(list_store) = panel.imp().list_store.get() else {
error!("list_store not initialized");
return;
};
list_store.append(&unit);
vacant_entry.insert(unit.clone());
if LoadState::Loaded == unit.load_state() {
let count = panel.loaded_units_count();
if count > 0 {
panel.set_loaded_units_count(count - 1)
}
}
if unit.enable_status() != UnitFileStatus::Unknown {
let count = panel.unit_files_count();
if count > 0 {
panel.set_unit_files_count(count - 1);
}
}
}
} else {
match systemd::get_unit_file_state(unit.dbus_level(), &unit.primary()) {
Ok(state) => unit.set_enable_status(state),
Err(SystemdErrors::ZFileNotFound(_)) => {
let Some(list_store) = panel.imp().list_store.get() else {
error!("list_store not initialized");
return;
};
let unit_name = unit.primary();
if let Some(position) = list_store.find_with_equal_func(|object| {
let Some(unit_item) = object.downcast_ref::<UnitInfo>() else {
error!("item.downcast_ref::<UnitInfo>()");
return false;
};
unit_name == unit_item.primary()
}) {
list_store.remove(position);
}
let key = UnitKey::new(&unit);
let mut unit_map = panel.imp().units_map.borrow_mut();
unit_map.remove(&key);
let count = panel.loaded_units_count();
panel.set_loaded_units_count(count + 1);
let count = panel.unit_files_count();
panel.set_unit_files_count(count + 1);
}
Err(err) => warn!("{:?}", err),
}
}
});
}
pub fn selected_unit(&self) -> Option<UnitInfo> {
self.unit.borrow().clone()
}
pub fn set_inter_message(&self, _action: &InterPanelMessage) {}
fn set_sorter(&self) {
let units_browser = units_browser!(self);
let sorter = units_browser.sorter();
self.unit_list_sort_list_model
.borrow()
.set_sorter(sorter.as_ref());
let col_def_list = self.current_column_view_column_definition_list.borrow();
if let Some((idx, col_def)) = col_def_list
.iter()
.enumerate()
.find(|(_, col_def)| col_def.sort() != save::SortType::Unset)
{
let first_col = units_browser.columns().item(idx as u32);
let idx_column = first_col.and_downcast_ref::<gtk::ColumnViewColumn>();
if let Some(sort_type) = col_def.sort().into() {
units_browser.sort_by_column(idx_column, sort_type);
}
} else {
let first_col = units_browser.columns().item(0);
let first_column = first_col.and_downcast_ref::<gtk::ColumnViewColumn>();
units_browser.sort_by_column(first_column, gtk::SortType::Ascending);
}
}
pub(super) fn filter_assessor_change(
&self,
id: &str,
new_assessor: Option<Box<dyn UnitPropertyAssessor>>,
change_type: Option<gtk::FilterChange>,
update_widget: bool,
) {
debug!("Assessor Change {new_assessor:?} Change Type: {change_type:?}");
let applied_assessors = self
.applied_unit_property_filters
.get()
.expect("applied_assessors not null");
let add = if let Some(new_assessor) = new_assessor {
debug!("Add filter id {id}");
update_search_entry!(self, id, update_widget, new_assessor.text());
let mut vect = applied_assessors.borrow_mut();
if let Some(index) = vect.iter().position(|x| x.id() == id) {
vect[index] = new_assessor;
} else {
vect.push(new_assessor);
}
true
} else {
debug!("Remove filter id {id}");
applied_assessors.borrow_mut().retain(|x| x.id() != id);
update_search_entry!(self, id, update_widget, "");
false
};
self.set_filter_column_header_marker(add, id);
if let Some(change_type) = change_type {
if let Some(filter) = self.filter_list_model.borrow().filter() {
filter.changed(change_type);
} else {
let custom_filter = self.create_custom_filter();
self.filter_list_model
.borrow()
.set_filter(Some(&custom_filter));
}
}
let search_controls = self.search_controls.get().expect("Not Null");
search_controls.set_filter_is_set(!applied_assessors.borrow().is_empty());
}
fn set_filter_column_header_marker(&self, add: bool, id: &str) {
if let Some(unit_prop_selection) = self
.current_column_view_column_definition_list
.borrow()
.iter()
.find(|unit_prop_selection| {
unit_prop_selection
.column()
.id()
.is_some_and(|cid| cid == id)
})
{
let col = unit_prop_selection.column();
let title = if let Some(title) = col.title() {
title.to_string()
} else {
"".to_string()
};
let new_title = if add {
if !title.starts_with(FILTER_MARK) {
format!("{FILTER_MARK} {title}")
} else {
title
}
} else {
get_clean_col_title(&title)
};
col.set_title(Some(&new_title));
}
}
pub(super) fn clear_filters(&self, filter_key: &str) {
for property_filter in self.unit_property_filters.borrow().values() {
let mut prop_filter_mut = property_filter.borrow_mut();
if filter_key == prop_filter_mut.id() || filter_key == ALL_FILTER_KEY {
prop_filter_mut.clear_n_apply_filter();
}
}
if filter_key == COL_ID_UNIT || filter_key == ALL_FILTER_KEY {
let search_controls = self.search_controls.get().expect("Not Null");
search_controls.imp().clear();
}
}
pub(super) fn button_action(&self, action: &InterPanelMessage) {
let Some(app_window) = self.app_window.get() else {
warn!("No app window");
return;
};
app_window.set_inter_message(action);
}
fn search_entry_set_text(&self, text: &str) {
let search_controls = self.search_controls.get().expect("Not Null");
search_controls.imp().set_search_entry_text(text);
}
pub(super) fn update_unit_name_search(&self, text: &str, update_widget: bool) {
debug!("update_unit_name_search {text}");
let Some(filter) = self.lazy_get_filter_assessor(&SysdColumn::Name) else {
error!("No filter id {COL_ID_UNIT}");
return;
};
let mut filter = filter.borrow_mut();
let filter = filter
.as_any_mut()
.downcast_mut::<FilterText>()
.expect("downcast to FilterText");
filter.set_filter_elem(text, update_widget);
}
fn create_custom_filter(&self) -> gtk::CustomFilter {
let applied_assessors = self
.applied_unit_property_filters
.get()
.expect("not none")
.clone();
gtk::CustomFilter::new(move |object| {
let Some(unit) = object.downcast_ref::<UnitInfo>() else {
error!("some wrong downcast_ref to UnitBinding {object:?}");
return false;
};
for asserror in applied_assessors.borrow().iter() {
if !asserror.filter_unit(unit) {
return false;
}
}
true
})
}
pub(super) fn lazy_get_filter_assessor(
&self,
id: &SysdColumn,
) -> Option<Rc<RefCell<Box<dyn UnitPropertyFilter>>>> {
let id_str = id.id();
{
if let Some(filter) = self.unit_property_filters.borrow().get(id_str) {
return Some(filter.clone());
}
}
let unit_list_panel: glib::BorrowedObject<'_, crate::widget::unit_list::UnitListPanel> =
self.obj();
let case_incensitive_default = self.case_incensitive_default.get();
let filter: Option<Box<dyn UnitPropertyFilter>> = match id {
SysdColumn::Name => Some(Box::new(FilterText::new(
id_str,
filter_unit_name,
&unit_list_panel,
case_incensitive_default,
))),
SysdColumn::Bus => Some(Box::new(FilterElement::new(
id_str,
filter_bus_level,
&unit_list_panel,
))),
SysdColumn::Type => Some(Box::new(FilterElement::new(
id_str,
filter_unit_type,
&unit_list_panel,
))),
SysdColumn::State => Some(Box::new(FilterElement::new(
id_str,
filter_enable_status,
&unit_list_panel,
))),
SysdColumn::Preset => Some(Box::new(FilterElement::new(
id_str,
filter_preset,
&unit_list_panel,
))),
SysdColumn::Load => Some(Box::new(FilterElement::new(
id_str,
filter_load_state,
&unit_list_panel,
))),
SysdColumn::Active => Some(Box::new(FilterElement::new(
id_str,
filter_active_state,
&unit_list_panel,
))),
SysdColumn::SubState => Some(Box::new(FilterElement::new(
id_str,
filter_sub_state,
&unit_list_panel,
))),
SysdColumn::Description => Some(Box::new(FilterText::new(
id_str,
filter_unit_description,
&unit_list_panel,
case_incensitive_default,
))),
_ => match id.property_type().as_deref() {
Some("t") => Some(Box::new(FilterNum::<u64>::new(
id_str,
custom_num::<u64>,
&unit_list_panel,
id.generate_quark(),
UnitPropertyFilterType::NumU64,
))),
Some("s") => Some(Box::new(FilterText::newq(
id_str,
custom_str,
&unit_list_panel,
case_incensitive_default,
id.generate_quark(),
))),
Some("i") => Some(Box::new(FilterNum::<i32>::new(
id_str,
custom_num::<i32>,
&unit_list_panel,
id.generate_quark(),
UnitPropertyFilterType::NumI32,
))),
Some("u") => Some(Box::new(FilterNum::<u32>::new(
id_str,
custom_num::<u32>,
&unit_list_panel,
id.generate_quark(),
UnitPropertyFilterType::NumU32,
))),
Some("b") => Some(Box::new(FilterBool::new(
id_str,
custom_bool,
&unit_list_panel,
id.generate_quark(),
))),
Some("q") => Some(Box::new(FilterNum::<u16>::new(
id_str,
custom_num::<u16>,
&unit_list_panel,
id.generate_quark(),
UnitPropertyFilterType::NumU16,
))),
Some("x") => Some(Box::new(FilterNum::<i64>::new(
id_str,
custom_num::<i64>,
&unit_list_panel,
id.generate_quark(),
UnitPropertyFilterType::NumI64,
))),
Some(&_) => Some(Box::new(FilterText::newq(
id_str,
custom_str,
&unit_list_panel,
case_incensitive_default,
id.generate_quark(),
))),
None => {
error!(
"Filtering for key {id:?} not handled yet, data type {:?}",
id.property_type()
);
None
}
},
};
if let Some(filter) = filter {
let mut unit_property_filters = self.unit_property_filters.borrow_mut();
let filter = Rc::new(RefCell::new(filter));
unit_property_filters.insert(id_str.to_string(), filter.clone());
Some(filter)
} else {
None
}
}
pub(super) fn set_new_columns(
&self,
property_list: Vec<UnitPropertySelection>,
fetch_custom_props: bool,
) {
if property_list.is_empty() {
warn!("Column list empty, Abort");
return;
}
if fetch_custom_props && self.selected_list_view.get() != UnitCuratedList::Custom {
let settings = systemd_gui::new_settings();
settings
.set_string(UnitCuratedList::base_action(), UnitCuratedList::Custom.id())
.inspect_err(|err| {
warn!(
"Set settings k {} v {} {err:?}",
UnitCuratedList::base_action(),
UnitCuratedList::Custom.id()
)
})
.unwrap();
return;
}
let columns_list_model = units_browser!(self).columns();
let cur_n_items = columns_list_model.n_items();
let mut current_columns_over = Vec::with_capacity(columns_list_model.n_items() as usize);
for position in (property_list.len() as u32)..columns_list_model.n_items() {
let Some(column) = columns_list_model
.item(position)
.and_downcast::<gtk::ColumnViewColumn>()
else {
warn!("Col None");
continue;
};
current_columns_over.push(column);
}
let units_browser = units_browser!(self);
for (idx, unit_property) in property_list.iter().enumerate() {
let new_column = unit_property.column();
let idx_32 = idx as u32;
if idx_32 < cur_n_items {
let Some(cur_column) = columns_list_model
.item(idx_32)
.and_downcast::<gtk::ColumnViewColumn>()
else {
warn!("Col None");
continue;
};
UnitPropertySelection::copy_col_to_col(&new_column, &cur_column);
unit_property.set_column(cur_column);
} else {
info!("Append {:?} {:?}", new_column.id(), new_column.title());
units_browser.append_column(&new_column);
}
}
self.current_column_view_column_definition_list
.replace(property_list);
for column in current_columns_over.iter() {
units_browser.remove_column(column);
}
force_expand_on_the_last_visible_column(&columns_list_model);
if fetch_custom_props {
self.fetch_custom_unit_properties();
}
}
fn fetch_custom_unit_properties(&self) {
let current_property_list = self.current_column_view_column_definition_list.borrow();
if current_property_list.is_empty() {
info!("No extra properties to fetch");
return;
}
info!("!!! Fetching custom unit properties !!!");
let current_property_list = current_property_list.clone();
let mut property_list_send = HashSet::with_capacity(current_property_list.len());
for unit_property_selection in current_property_list.iter() {
unit_property_selection.fill_property_fetcher(&mut property_list_send)
}
let units_browser = units_browser!(self).clone();
let units_map = self.units_map.clone();
let display_color = self.display_color.get();
let unit_list = self.obj().clone();
let Some(list_store) = self.list_store.get() else {
error!("list_store not initialized");
return;
};
let list_store = list_store.clone();
glib::spawn_future_local(async move {
let units_list: Vec<_> = units_map
.borrow()
.iter()
.filter(|(_, unit)| !unit.is_template_unit_file())
.map(|(key, unit)| {
(
unit.dbus_level(),
unit.primary(),
unit.object_path(),
unit.unit_type(),
key.update_properties.get(),
)
})
.collect();
let (sender, mut receiver) = tokio::sync::mpsc::channel(100);
let handle = systemd::runtime().spawn(async move {
info!("Fetching properties START for {} units", units_list.len());
for (level, primary_name, object_path, unit_type, update_property_flag) in
units_list.into_iter()
{
let mut cleaned_props: Vec<_> = Vec::with_capacity(property_list_send.len());
for unitcol in property_list_send
.iter()
.filter(|item| item.utype() == UnitType::Unit || item.utype() == unit_type)
{
cleaned_props.push((
unitcol.utype(),
unitcol.property(),
unitcol.generate_quark(),
));
}
debug!("orignal {:?}", property_list_send);
debug!("cleaned {:?}", cleaned_props);
let properties_setter = systemd::fetch_unit_properties(
level,
&primary_name,
&object_path,
update_property_flag,
cleaned_props,
)
.await
.inspect_err(|err| debug!("Some Error : {err:?}"))
.unwrap_or(vec![]);
let result = sender
.send((UnitKey::new_string(level, primary_name), properties_setter))
.await;
if let Err(err) = result {
error!("The channel needs to be open. {err:?}");
break;
}
}
});
unit_list.imp().add_tokio_handle(handle);
info!("Fetching properties WAIT");
while let Some((key, property_value_list)) = receiver.recv().await {
let map_ref = units_map.borrow();
let Some(unit) = map_ref.get(&key) else {
warn!("UnitKey not Found: {key:?}");
continue;
};
for setter in property_value_list {
match setter {
UnitPropertySetter::FileState(unit_file_status) => {
unit.set_enable_status(unit_file_status)
}
UnitPropertySetter::Description(description) => {
unit.set_description(description)
}
UnitPropertySetter::ActiveState(active_state) => {
unit.set_active_state(active_state)
}
UnitPropertySetter::LoadState(load_state) => {
unit.set_load_state(load_state)
}
UnitPropertySetter::FragmentPath(_) => {}
UnitPropertySetter::UnitFilePreset(preset) => unit.set_preset(preset),
UnitPropertySetter::SubState(substate) => unit.set_sub_state(substate),
UnitPropertySetter::Custom(quark, owned_value) => {
trace!("Custom Prop key: {:?} value: {:?}", quark, owned_value);
if &quark
== SOCKET_LISTEN_QUARK
.get_or_init(|| SysdColumn::SocketListen.generate_quark())
{
let listens = unit.insert_socket_listen(quark, owned_value);
for idx in 1..listens {
let usocket = SocketUnitInfo::from_unit_socket(unit, idx);
list_store.append(&usocket);
}
} else if &quark
== PATH_QUARK.get_or_init(|| SysdColumn::Path.generate_quark())
{
let _listens = unit.insert_socket_listen(quark, owned_value);
} else {
unit.insert_unit_property_value(quark, owned_value)
}
}
}
}
}
info!("Fetching properties FINISHED");
for column in units_browser
.columns()
.iter::<gtk::ColumnViewColumn>()
.filter_map(|item| item.ok())
{
let prop_type = current_property_list
.iter()
.find(|prop_selection| prop_selection.id() == column.id())
.and_then(|prop_selection| prop_selection.sysd_column());
if let Some(sysd_col) = prop_type {
construct::set_column_factory_and_sorter(&column, display_color, &sysd_col);
}
}
unit_list.imp().set_sorter();
});
}
pub fn print_scroll_adj_logs(&self) {
let va = self.scrolled_window.vadjustment();
self.print_adjustment("VER", &va);
let va = self.scrolled_window.hadjustment();
self.print_adjustment("HON", &va);
}
fn print_adjustment(&self, id: &str, adj: &Adjustment) {
info!(
"{} lower={} + page={} <= upper={} gap {} step_inc {} page_inc {}",
id,
adj.lower(),
adj.upper(),
adj.page_size(),
adj.upper() - (adj.lower() + adj.page_size()),
adj.step_increment(),
adj.page_increment()
);
}
pub(super) fn current_columns(&self) -> Ref<'_, Vec<UnitPropertySelection>> {
self.current_column_view_column_definition_list.borrow()
}
pub(super) fn current_columns_mut(&self) -> RefMut<'_, Vec<UnitPropertySelection>> {
self.current_column_view_column_definition_list.borrow_mut()
}
pub(super) fn columns(&self) -> gio::ListModel {
units_browser!(self).columns()
}
pub(super) fn default_displayed_columns(&self) -> &Vec<UnitPropertySelection> {
self.default_column_view_column_definition_list
.get_or_init(|| construct::default_column_definition_list(self.display_color.get()))
}
pub(super) fn save_config(&self) {
let view = self.selected_list_view.get();
save::save_column_config(
Some(&units_browser!(self).columns()),
&mut self.current_columns_mut(),
view,
);
}
fn add_tokio_handle(&self, handle: tokio::task::JoinHandle<()>) {
let ah = handle.abort_handle();
self.abort_handles.borrow_mut().push(ah);
}
fn aborts_handles(&self) {
for ah in self.abort_handles.borrow().iter() {
ah.abort();
}
}
fn set_loaded_units_count(&self, count: u32) {
let label = if count == 0 {
Cow::from("")
} else {
Cow::from(count.to_string())
};
self.loaded_units_count_label.set_label(&label);
self.loaded_units_count.set(count);
}
fn set_unit_files_count(&self, count: u32) {
let label = if count == 0 {
Cow::from("")
} else {
Cow::from(count.to_string())
};
self.unit_files_count_label.set_label(&label);
self.unit_files_count.set(count);
}
fn set_unit_filter_count(&self, count: u32) {
let label = if count == 0 {
Cow::from("")
} else {
Cow::from(count.to_string())
};
self.unit_filtered_count_label.set_label(&label);
self.unit_filtered_count.set(count);
if count == 0 && (self.unit_files_count.get() + self.loaded_units_count.get()) != 0 {
self.panel_stack
.set_visible_child_name(RESTRICTIVE_FILTER_VIEW_PAGE);
} else if self.panel_stack.visible_child_name().as_deref()
== Some(RESTRICTIVE_FILTER_VIEW_PAGE)
{
self.panel_stack.set_visible_child_name(UNIT_LIST_VIEW_PAGE);
}
}
fn set_case_incensitive_default(&self, case_incensitive_default: bool) {
if self.case_incensitive_default.get() == case_incensitive_default {
return;
}
self.case_incensitive_default.set(case_incensitive_default);
for filter in self.unit_property_filters.borrow().values() {
let mut filter = filter.borrow_mut();
let Some(filter_text) = filter.as_any_mut().downcast_mut::<FilterText>() else {
continue;
};
filter_text.set_filter_match_case_insensitive(case_incensitive_default, false);
}
}
fn retreive_favorites(&self) {
let list_panel = self.obj().clone();
glib::spawn_future_local(async move {
let (sender, receiver) = tokio::sync::oneshot::channel();
systemd::runtime().spawn(async move {
let favorites = favorites::load_favorites().map(|favorites| {
favorites
.favorites
.into_iter()
.map(|favorite| UnitKey::new_string(favorite.bus.into(), favorite.unit))
.collect::<Vec<_>>()
});
if sender.send(favorites).is_err() {
warn!("Error send favorites");
}
});
let Ok(Some(returned_fav)) = receiver
.await
.inspect_err(|err| error!("Tokio channel dropped {err:?}"))
else {
return;
};
#[allow(clippy::mutable_key_type)]
let favorites_map = returned_fav.into_iter().map(|key| (key, None)).collect();
let list_panel_imp = list_panel.imp();
list_panel_imp.favorites.replace(favorites_map);
if list_panel_imp.selected_list_view.get() == UnitCuratedList::Favorites
&& let Err(err) = list_panel.activate_action(ACTION_WIN_REFRESH_UNIT_LIST, None)
{
warn!("call action {ACTION_WIN_REFRESH_UNIT_LIST} error: {err}");
}
});
}
pub(super) fn is_favorite(&self, unit: &UnitInfo) -> bool {
let key = UnitKey::new(unit);
self.favorites.borrow().contains_key(&key)
}
fn process_signals(&self) {
let list_panel = self.obj().clone();
glib::spawn_future_local(async move {
let (sender, mut receiver) = mpsc::channel(100);
let _handle = runtime().spawn(async { unit_load_batch(sender).await });
while let Some(signal) = receiver.recv().await {
info!("{:?}", signal);
list_panel.imp().manage_unit_new_remove(signal);
}
});
}
}
async fn unit_load_batch(sender: mpsc::Sender<SystemdSignal>) {
let systemd_signal_receiver: Receiver<systemd::SystemdSignal> =
init_signal_watcher(UnitDBusLevel::Both).await;
let stream = tokio_stream::wrappers::BroadcastStream::new(systemd_signal_receiver);
let batch_stream = stream
.filter(|batch_result| {
matches!(
batch_result,
Ok(SystemdSignal::UnitNew(_, _)) | Ok(SystemdSignal::UnitRemoved(_, _,))
)
})
.chunks_timeout(100, Duration::from_millis(500));
tokio::pin!(batch_stream);
while let Some(signals) = batch_stream.next().await {
let mut set = HashSet::with_capacity(signals.len());
for signal in signals.into_iter().filter_map(|result| result.ok()) {
let toggled = signal.toggle_unit();
if !set.remove(&toggled) {
set.insert(toggled.toggle_unit());
}
}
for signal in set {
if let Err(err) = sender.send(signal).await {
warn!("Sender Unit New/Removed Error {:?}", err);
}
}
}
info!("Signal Browser End receiving signals")
}
macro_rules! dbus_call {
($int_level:expr, $handles:expr, $module:ident :: $f:ident) => {{
if matches!($int_level, DbusLevel::System | DbusLevel::SystemAndSession) {
$handles.push(tokio::spawn($module::$f(UnitDBusLevel::System)));
}
if matches!(
$int_level,
DbusLevel::UserSession | DbusLevel::SystemAndSession
) {
$handles.push(tokio::spawn($module::$f(UnitDBusLevel::UserSession)));
}
}};
}
impl UnitListPanelImp {
async fn retrieve_unit_list(
&self,
int_level: DbusLevel,
view: UnitCuratedList,
unit_list: &UnitListPanel,
) -> Result<Vec<ListUnitResponse>, bool> {
let (sender_syst, receiver_syst) = tokio::sync::oneshot::channel();
let include_unit_files = unit_list.imp().include_unit_files.get();
let handle = if view == UnitCuratedList::Favorites {
let mut system: Vec<String> = Vec::new();
let mut user_session: Vec<String> = Vec::new();
for key in self.favorites.borrow().keys() {
if key.level == UnitDBusLevel::System {
system.push(key.primary.clone());
} else {
user_session.push(key.primary.clone());
}
}
systemd::runtime().spawn(async move {
let mut handles = Vec::with_capacity(4);
if !system.is_empty() {
handles.push(tokio::spawn(systemd::list_loaded_units_list(
UnitDBusLevel::System,
system.clone(),
)));
handles.push(tokio::spawn(systemd::list_unit_files_list(
UnitDBusLevel::System,
system,
)));
}
if !user_session.is_empty() {
handles.push(tokio::spawn(systemd::list_loaded_units_list(
UnitDBusLevel::UserSession,
user_session.clone(),
)));
handles.push(tokio::spawn(systemd::list_unit_files_list(
UnitDBusLevel::UserSession,
user_session,
)));
}
send_unit_list(sender_syst, handles).await;
})
} else {
systemd::runtime().spawn(async move {
let mut handles = Vec::with_capacity(4);
match view {
UnitCuratedList::Defaut
| UnitCuratedList::Custom
| UnitCuratedList::LoadedUnit => {
dbus_call!(int_level, handles, systemd::list_loaded_units)
}
UnitCuratedList::Timers => {
dbus_call!(int_level, handles, systemd::list_loaded_units_timers)
}
UnitCuratedList::Sockets => {
dbus_call!(int_level, handles, systemd::list_loaded_units_sockets)
}
UnitCuratedList::Path => {
dbus_call!(int_level, handles, systemd::list_loaded_units_paths)
}
UnitCuratedList::Automount => {
dbus_call!(int_level, handles, systemd::list_loaded_units_automounts)
}
UnitCuratedList::Services => {
dbus_call!(int_level, handles, systemd::list_loaded_units_services)
}
UnitCuratedList::Favorites | UnitCuratedList::UnitFiles => {}
}
match view {
UnitCuratedList::Defaut
| UnitCuratedList::Custom
| UnitCuratedList::UnitFiles => {
dbus_call!(int_level, handles, systemd::list_unit_files)
}
UnitCuratedList::Timers if include_unit_files => {
dbus_call!(int_level, handles, systemd::list_unit_files_timers)
}
UnitCuratedList::Services if include_unit_files => {
dbus_call!(int_level, handles, systemd::list_unit_files_services)
}
UnitCuratedList::Sockets if include_unit_files => {
dbus_call!(int_level, handles, systemd::list_unit_files_sockets)
}
UnitCuratedList::Path if include_unit_files => {
dbus_call!(int_level, handles, systemd::list_unit_files_paths)
}
UnitCuratedList::Automount if include_unit_files => {
dbus_call!(int_level, handles, systemd::list_unit_files_automounts)
}
UnitCuratedList::Favorites
| UnitCuratedList::LoadedUnit
| UnitCuratedList::Timers
| UnitCuratedList::Services
| UnitCuratedList::Sockets
| UnitCuratedList::Path
| UnitCuratedList::Automount => {}
}
send_unit_list(sender_syst, handles).await;
})
};
unit_list.imp().add_tokio_handle(handle);
receiver_syst.await.unwrap_or_else(|err| {
error!("Tokio receiver works, {err:?}");
Err(true)
})
}
}
async fn send_unit_list(
sender_syst: tokio::sync::oneshot::Sender<Result<Vec<ListUnitResponse>, bool>>,
handles: Vec<tokio::task::JoinHandle<Result<ListUnitResponse, systemd::errors::SystemdErrors>>>,
) {
let mut results = Vec::with_capacity(handles.len());
let mut error = false;
for handle in handles {
let Ok(r) = handle
.await
.inspect_err(|err| warn!("Unit List Join Error: {err:?}"))
else {
error = true;
continue;
};
let Ok(x) = r.inspect_err(|err| warn!("Unit List Call Error: {err:?}")) else {
error = true;
continue;
};
results.push(x);
}
let result = if error { Err(error) } else { Ok(results) };
sender_syst
.send(result)
.expect("The channel needs to be open.");
}
fn force_expand_on_the_last_visible_column(columns_list_model: &gio::ListModel) {
if let Some(column) = columns_list_model
.iter::<gtk::ColumnViewColumn>()
.rev()
.filter_map(|item| item.ok())
.next()
{
column.set_expand(true);
}
}
#[glib::object_subclass]
impl ObjectSubclass for UnitListPanelImp {
const NAME: &'static str = "UnitListPanel";
type Type = super::UnitListPanel;
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();
}
}
#[glib::derived_properties]
impl ObjectImpl for UnitListPanelImp {
fn constructed(&self) {
self.parent_constructed();
let settings = systemd_gui::new_settings();
let unit_list = self.obj().clone();
settings
.bind(
KEY_PREF_UNIT_LIST_DISPLAY_COLORS,
&unit_list,
"display-color",
)
.build();
settings
.bind::<UnitListPanel>(
&WIN_ACTION_INCLUDE_UNIT_FILES[4..],
&self.obj(),
&WIN_ACTION_INCLUDE_UNIT_FILES[4..],
)
.build();
let unit_list_panel = self.obj().clone();
let list_store = gio::ListStore::new::<UnitInfo>();
self.list_store
.set(list_store.clone())
.expect("Set only Once");
let view = self.selected_list_view.get();
info!("Selected Browser View : {:?}", view);
let sort_list_model = gtk::SortListModel::new(Some(list_store), None::<gtk::Sorter>);
let filter_list_model =
gtk::FilterListModel::new(Some(sort_list_model.clone()), None::<gtk::Filter>);
let single_selection = gtk::SingleSelection::builder()
.model(&filter_list_model)
.autoselect(false)
.build();
let column_view = gtk::ColumnView::new(Some(single_selection.clone()));
let column_view_column_definition_list = construct::construct_column_view(
self.display_color.get(),
view,
self.include_unit_files.get(),
);
for unit_property_selection in column_view_column_definition_list.iter() {
column_view.append_column(&unit_property_selection.column());
}
let sorter = column_view.sorter();
sort_list_model.set_sorter(sorter.as_ref());
self.scrolled_window.set_child(Some(&column_view));
self.units_browser.get_or_init(|| column_view);
self.single_selection.get_or_init(|| single_selection);
self.filter_list_model.replace(filter_list_model);
self.unit_list_sort_list_model.replace(sort_list_model);
let column_view_column_list = self.generate_column_list();
self.current_column_view_column_definition_list
.replace(column_view_column_definition_list);
let current_column_view_column_definition_list =
self.current_column_view_column_definition_list.borrow();
column_factories::setup_factories(
&unit_list_panel,
&column_view_column_list,
¤t_column_view_column_definition_list,
);
settings.connect_changed(
Some(KEY_PREF_UNIT_LIST_DISPLAY_COLORS),
move |_settings, _key| {
let display_color = unit_list_panel.display_color();
info!("Change preference setting \"display color\" to {display_color}");
let column_view_column_list = unit_list_panel.imp().generate_column_list();
let current_column_view_column_definition_list = unit_list_panel
.imp()
.current_column_view_column_definition_list
.borrow();
column_factories::setup_factories(
&unit_list_panel,
&column_view_column_list,
¤t_column_view_column_definition_list,
);
},
);
let _ = self
.applied_unit_property_filters
.set(Rc::new(RefCell::new(Vec::new())));
let custom_filter = self.create_custom_filter();
self.filter_list_model
.borrow()
.set_filter(Some(&custom_filter));
self.filter_list_model
.borrow()
.bind_property::<UnitListPanel>("n-items", &self.obj(), "unit_filtered_count")
.build();
let search_controls = UnitListSearchControls::new(&self.obj());
self.search_bar.set_child(Some(&search_controls));
self.search_controls
.set(search_controls)
.expect("Search entry set once");
let units_browser = units_browser!(self);
{
let unit_list = self.obj().clone();
let units_browser = units_browser.clone();
self.scrolled_window
.vadjustment()
.connect_changed(move |_adjustment| {
focus_on_row(&unit_list, &units_browser);
});
}
settings
.bind(
&KEY_PREF_UNIT_LIST_DISPLAY_SUMMARY[4..],
&self.summary.get(),
"visible",
)
.build();
units_browser.connect_activate(|_a, row| info!("Unit row position {row}"));
let pop = pop_menu::UnitPopMenu::new(
units_browser,
&self.obj(),
&self.filter_list_model.borrow(),
);
let _ = self.pop_menu.set(pop);
force_expand_on_the_last_visible_column(&units_browser.columns());
settings
.bind::<UnitListPanel>(
KEY_PREF_CASE_INSENSITIVE_DEFAULT,
&self.obj(),
"case-incensitive-default",
)
.build();
self.retreive_favorites();
self.process_signals();
}
}
fn focus_on_row(unit_list: &super::UnitListPanel, units_browser: >k::ColumnView) {
let Some(mut force_selected_index) = unit_list.imp().force_selected_index.get() else {
return;
};
unit_list.imp().force_selected_index.set(None);
if force_selected_index == gtk::INVALID_LIST_POSITION {
force_selected_index = 0;
}
info!("Focus on selected unit list row (index {force_selected_index})");
let units_browser = units_browser.clone();
glib::spawn_future_local(async move {
gio::spawn_blocking(move || {
let sleep_duration = Duration::from_millis(100);
std::thread::sleep(sleep_duration);
})
.await
.expect("Task needs to finish successfully.");
units_browser.scroll_to(
force_selected_index, None,
gtk::ListScrollFlags::FOCUS,
None,
);
});
}
impl WidgetImpl for UnitListPanelImp {}
impl BoxImpl for UnitListPanelImp {}
#[cfg(test)]
mod test {
#[test]
fn test_reverse() {
for i in (0..10).rev() {
println!("{i}")
}
}
}