use crate::widget::app_window::AppWindow;
use adw::subclass::prelude::ObjectSubclassIsExt;
use gtk::glib::{self};
glib::wrapper! {
pub struct ListBootsWindow(ObjectSubclass<imp::ListBootsWindowImp>)
@extends adw::Window, gtk::Window, gtk::Widget,
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget,
gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl ListBootsWindow {
pub fn new(app_window: &AppWindow) -> Self {
let obj: ListBootsWindow = glib::Object::new();
let _ = obj.imp().app_window.set(app_window.clone());
obj.imp().fill_store();
obj
}
}
mod imp {
const WINDOW_HEIGHT: &str = "list-boots-window-height";
const WINDOW_WIDTH: &str = "list-boots-window-width";
use super::ListBootsWindow;
use crate::{
systemd::{self, BootFilter, data::UnitInfo},
systemd_gui::new_settings,
widget::{
InterPanelMessage, app_window::AppWindow, close_window_shortcut,
preferences::data::PREFERENCES,
},
};
use adw::subclass::window::AdwWindowImpl;
use gio::{glib::BoxedAnyObject, prelude::ListModelExt};
use gtk::{
glib::{self},
prelude::*,
subclass::{
prelude::*,
widget::{CompositeTemplateClass, CompositeTemplateInitializingExt, WidgetImpl},
},
};
use std::{
cell::{OnceCell, Ref},
collections::HashMap,
ops::DerefMut,
rc::Rc,
};
use systemd::journal_data::Boot;
use tracing::{debug, error, info, warn};
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/list_boots.ui")]
pub struct ListBootsWindowImp {
#[template_child]
list_store: TemplateChild<gio::ListStore>,
#[template_child]
boots_browser: TemplateChild<gtk::ColumnView>,
#[template_child]
stack: TemplateChild<adw::ViewStack>,
#[template_child]
list_boots_sort_list_model: TemplateChild<gtk::SortListModel>,
pub(super) app_window: OnceCell<AppWindow>,
}
#[glib::object_subclass]
impl ObjectSubclass for ListBootsWindowImp {
const NAME: &'static str = "ListBoots";
type Type = ListBootsWindow;
type ParentType = adw::Window;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
impl ObjectImpl for ListBootsWindowImp {
fn constructed(&self) {
self.parent_constructed();
let list_boots_windows = self.obj();
close_window_shortcut(list_boots_windows.as_ref());
self.load_window_size();
let map = self.generate_column_map();
set_up_factories(&map, &list_boots_windows);
}
}
impl ListBootsWindowImp {
pub(super) fn fill_store(&self) {
let stack = self.stack.clone();
let list_store = self.list_store.clone();
let Some(app_window) = self.app_window.get() else {
error!("app_window not initialized");
return;
};
let app_window = app_window.clone();
let window = self.obj().clone();
glib::spawn_future_local(async move {
stack.set_visible_child_name("spinner");
list_store.remove_all();
if app_window.imp().cached_list_boots().as_ref().is_none() {
let boots = gio::spawn_blocking(systemd::list_boots)
.await
.expect("Task needs to finish successfully.");
let boots = match boots {
Ok(boots) => {
let boots: Vec<Rc<Boot>> = boots.into_iter().map(Rc::new).collect();
boots
}
Err(error) => {
warn!("List boots Error {error:?}");
return;
}
};
app_window.imp().update_list_boots(boots);
} else {
let last_time = gio::spawn_blocking(systemd::fetch_last_time)
.await
.expect("Task needs to finish successfully.");
let last_time = match last_time {
Ok(last_time) => last_time,
Err(error) => {
warn!("Fetch_last_time Error {error:?}");
return;
}
};
let mut binding = app_window.imp().cached_list_boots_mut();
if let Some(boots) = binding.deref_mut()
&& let Some(boot) = boots.pop()
{
let new_boot = Boot {
boot_id: boot.boot_id.clone(),
last: last_time,
..*boot.as_ref()
};
boots.push(Rc::new(new_boot));
}
}
let binding = app_window.imp().cached_list_boots();
let Some(boots) = binding.as_ref() else {
warn!("Something wrong");
return;
};
for boot in boots.iter() {
let bx = BoxedAnyObject::new(boot.clone());
list_store.append(&bx);
}
window.imp().set_sorter();
stack.set_visible_child_name("list_boots");
});
}
fn generate_column_map(&self) -> HashMap<glib::GString, gtk::ColumnViewColumn> {
let list_model: gio::ListModel = self.boots_browser.columns();
let mut col_map = HashMap::new();
for column_view_column in list_model
.iter::<gtk::ColumnViewColumn>()
.filter_map(|item| item.ok())
{
column_view_column.connect_fixed_width_notify(|column| {
debug!("{:?} {}", column.id(), column.fixed_width())
});
if let Some(id) = column_view_column.id() {
col_map.insert(id, column_view_column.clone());
} else {
warn!("Column has no id.")
}
}
col_map
}
pub fn save_window_context(&self) -> Result<(), glib::BoolError> {
let obj = self.obj();
let (width, height) = obj.default_size();
let settings = crate::systemd_gui::new_settings();
settings.set_int(WINDOW_WIDTH, width)?;
settings.set_int(WINDOW_HEIGHT, height)?;
Ok(())
}
fn load_window_size(&self) {
let settings = new_settings();
let mut width = settings.int(WINDOW_WIDTH);
let mut height = settings.int(WINDOW_HEIGHT);
let obj = self.obj();
let (def_width, def_height) = obj.default_size();
if width < 0 {
width = def_width;
if width < 0 {
width = 1280;
}
}
if height < 0 {
height = def_height;
if height < 0 {
height = 720;
}
}
obj.set_default_size(width, height);
}
fn set_sorter(&self) {
let sorter = self.boots_browser.sorter();
self.list_boots_sort_list_model.set_sorter(sorter.as_ref());
let item_out = self
.boots_browser
.columns()
.item(0)
.expect("Expect item x to be not None");
let c1 = item_out
.downcast_ref::<gtk::ColumnViewColumn>()
.expect("item.downcast_ref::<gtk::ColumnViewColumn>()");
self.boots_browser
.sort_by_column(Some(c1), gtk::SortType::Descending);
}
fn selected_unit(&self) -> Option<UnitInfo> {
let app_window = self.app_window.get()?;
app_window.selected_unit()
}
}
macro_rules! compare_boots {
($boot1:expr, $boot2:expr, $func:ident) => {{
$boot1.$func().cmp(&$boot2.$func()).into()
}};
($boot1:expr, $boot2:expr, $func:ident, $($funcx:ident),+) => {{
let ordering = $boot1.$func().cmp(&$boot2.$func());
if ordering != core::cmp::Ordering::Equal {
return ordering.into();
}
compare_boots!($boot1, $boot2, $($funcx),+)
}};
}
macro_rules! create_column_filter {
($($func:ident),+) => {{
gtk::CustomSorter::new(move |obj1, obj2| {
let boxed = obj1.downcast_ref::<BoxedAnyObject>().unwrap();
let boot1: Ref<Rc<Boot>> = boxed.borrow();
let boxed = obj2.downcast_ref::<BoxedAnyObject>().unwrap();
let boot2: Ref<Rc<Boot>> = boxed.borrow();
compare_boots!(boot2, boot1, $($func),+)
})
}};
}
macro_rules! column_view_column_set_sorter {
($column_view_column:expr, $($func:ident),+) => {{
let sorter = create_column_filter!($($func),+);
$column_view_column.set_sorter(Some(&sorter));
}};
}
fn setup<'a>(
column_view_column_map: &'a HashMap<glib::GString, gtk::ColumnViewColumn>,
key: &str,
) -> (gtk::SignalListItemFactory, &'a gtk::ColumnViewColumn) {
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(move |_factory, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let row = gtk::Label::builder()
.selectable(true)
.xalign(0.0)
.build();
item.set_child(Some(&row));
});
let col = column_view_column_map.get(key).unwrap();
col.set_factory(Some(&factory));
(factory, col)
}
fn setup_action(
column_view_column_map: &HashMap<glib::GString, gtk::ColumnViewColumn>,
list_boots_windows: &ListBootsWindow,
) {
let factory = gtk::SignalListItemFactory::new();
factory.connect_setup(move |_factory, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let row = gtk::Button::builder()
.icon_name("funnel-symbolic")
.tooltip_text("Filter Journal Events")
.css_classes(["suggested-action", "circular"])
.margin_end(10)
.build();
item.set_child(Some(&row));
});
let col = column_view_column_map.get("action").unwrap();
col.set_factory(Some(&factory));
let list_boots_windows = list_boots_windows.clone();
factory.connect_bind(move |_factory, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let child = item.child().and_downcast::<gtk::Button>().unwrap();
let entry = item.item().and_downcast::<BoxedAnyObject>().unwrap();
let boot: Ref<Rc<Boot>> = entry.borrow();
{
let boot_id: String = boot.boot_id.clone();
let list_boots_windows = list_boots_windows.clone();
child.connect_clicked(move |_button| {
info!("boot {boot_id}");
let Some(app_window) = list_boots_windows.imp().app_window.get() else {
warn!("No app window");
return;
};
app_window.set_inter_message(&InterPanelMessage::JournalFilterBoot(
BootFilter::Id(boot_id.clone()),
));
});
}
if let Some(_unit) = list_boots_windows.imp().selected_unit() {
child.set_sensitive(true);
} else {
child.set_sensitive(false);
}
});
}
macro_rules! bind {
($factory:expr, $body:expr) => {{
$factory.connect_bind(move |_factory, item| {
let item = item.downcast_ref::<gtk::ListItem>().unwrap();
let child = item.child().and_downcast::<gtk::Label>().unwrap();
let entry = item.item().and_downcast::<BoxedAnyObject>().unwrap();
let boot: Ref<Rc<Boot>> = entry.borrow();
($body)(child, boot)
});
}};
}
fn set_up_factories(
column_view_column_map: &HashMap<glib::GString, gtk::ColumnViewColumn>,
list_boots_windows: &ListBootsWindow,
) {
let (col1factory, col1) = setup(column_view_column_map, "pos_offset");
column_view_column_set_sorter!(col1, index);
let (col1bfactory, col1b) = setup(column_view_column_map, "neg_offset");
column_view_column_set_sorter!(col1b, neg_offset);
let (col2factory, _) = setup(column_view_column_map, "boot_id");
let (col3factory, _) = setup(column_view_column_map, "firstlog");
let (col4factory, _) = setup(column_view_column_map, "lastlog");
let (col5factory, col5) = setup(column_view_column_map, "duration");
setup_action(column_view_column_map, list_boots_windows);
column_view_column_set_sorter!(col5, duration);
let bada = |child: gtk::Label, boot: Ref<Rc<Boot>>| child.set_text(&boot.index.to_string());
bind!(col1factory, bada);
let bada =
|child: gtk::Label, boot: Ref<Rc<Boot>>| child.set_text(&boot.neg_offset().to_string());
bind!(col1bfactory, bada);
let bada =
|child: gtk::Label, boot: Ref<Rc<Boot>>| child.set_text(&boot.boot_id.to_string());
bind!(col2factory, bada);
let timestamp_style = PREFERENCES.timestamp_style();
let bada = move |child: gtk::Label, boot: Ref<Rc<Boot>>| {
let time = systemd::time_handling::get_since_time(boot.first, timestamp_style);
child.set_text(&time);
};
bind!(col3factory, bada);
let bada = move |child: gtk::Label, boot: Ref<Rc<Boot>>| {
let time = systemd::time_handling::get_since_time(boot.last, timestamp_style);
child.set_text(&time);
};
bind!(col4factory, bada);
let bada = |child: gtk::Label, boot: Ref<Rc<Boot>>| {
let duration =
systemd::time_handling::format_timestamp_relative_duration(boot.first, boot.last);
child.set_text(&duration);
};
bind!(col5factory, bada);
}
impl WidgetImpl for ListBootsWindowImp {}
impl WindowImpl for ListBootsWindowImp {
fn close_request(&self) -> glib::Propagation {
debug!("Close window");
if let Err(_err) = self.save_window_context() {
error!("Failed to save window state");
}
self.parent_close_request();
glib::Propagation::Proceed
}
}
impl AdwWindowImpl for ListBootsWindowImp {}
}