use crate::{
consts::{
ACTION_APP_CREATE_UNIT, ACTION_DAEMON_RELOAD, ACTION_FIND_IN_TEXT_OPEN, ACTION_LIST_BOOT,
ACTION_PROPERTIES_SELECTOR, ACTION_PROPERTIES_SELECTOR_GENERAL,
ACTION_UNIT_PROPERTIES_DISPLAY, ACTION_WIN_REFRESH_UNIT_LIST, APP_ACTION_LIST_BOOT,
APP_ACTION_PROPERTIES_SELECTOR_GENERAL, APP_ACTION_SEARCH_UNITS,
APP_ACTION_UNIT_PROPERTIES_DISPLAY, WIN_ACTION_SAVE_UNIT_FILE,
},
systemd::{data::UnitInfo, journal_data::Boot},
systemd_gui::{self},
widget::{
InterPanelMessage,
creator::UnitCreatorWindow,
info_window::InfoWindow,
journal::list_boots::ListBootsWindow,
preferences::data::{DbusLevel, KEY_PREF_ORIENTATION_MODE, OrientationMode, PREFERENCES},
replace_tags,
signals_dialog::SignalsWindow,
unit_control_panel::UnitControlPanel,
unit_list::{UnitCuratedList, UnitListPanel},
unit_properties_selector::UnitPropertiesSelectorDialog,
},
};
use adw::subclass::prelude::*;
use glib::{self, VariantTy, closure::IntoClosureReturnValue, types::StaticType, value::ToValue};
use gtk::{
gio::{self, prelude::*},
prelude::*,
};
use std::{
borrow::Cow,
cell::{Cell, Ref, RefCell, RefMut},
rc::Rc,
};
use strum::IntoEnumIterator;
use tracing::{debug, info, warn};
const WINDOW_WIDTH: &str = "window-width";
const WINDOW_HEIGHT: &str = "window-height";
const PANED_SEPARATOR_POSITION: &str = "paned-separator-position";
const WINDOW_PANES_ORIENTATION: &str = "window-panes-orientation";
const IS_MAXIMIZED: &str = "is-maximized";
const HORIZONTAL: &str = "horizontal";
const VERTICAL: &str = "vertical";
const DEFAULT_WIDTH: i32 = 1280;
const DEFAULT_HEIGHT: i32 = 720;
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/app_window.ui")]
pub struct AppWindowImpl {
#[template_child]
header_bar: TemplateChild<adw::HeaderBar>,
#[template_child]
pub(super) toast_overlay: TemplateChild<adw::ToastOverlay>,
#[template_child]
unit_list_panel: TemplateChild<UnitListPanel>,
#[template_child]
unit_control_panel: TemplateChild<UnitControlPanel>,
#[template_child]
paned: TemplateChild<gtk::Paned>,
#[template_child]
search_toggle_button: TemplateChild<gtk::ToggleButton>,
#[template_child]
refresh_unit_list_button: TemplateChild<gtk::Button>,
#[template_child]
system_session_dropdown: TemplateChild<gtk::DropDown>,
#[template_child]
unit_list_view_menubutton: TemplateChild<gtk::MenuButton>,
#[template_child]
app_title: TemplateChild<adw::WindowTitle>,
#[template_child]
breakpoint: TemplateChild<adw::Breakpoint>,
orientation_mode: Cell<OrientationMode>,
list_boots: RefCell<Option<Vec<Rc<Boot>>>>,
pub(super) selected_unit: RefCell<Option<UnitInfo>>,
pub signals_window: RefCell<Option<SignalsWindow>>,
}
#[glib::object_subclass]
impl ObjectSubclass for AppWindowImpl {
const NAME: &'static str = "SysdMainAppWindow";
type Type = super::AppWindow;
type ParentType = adw::ApplicationWindow;
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 AppWindowImpl {
fn constructed(&self) {
self.parent_constructed();
let settings = systemd_gui::new_settings();
{
let app_window = self.obj().clone();
settings.connect_changed(Some(KEY_PREF_ORIENTATION_MODE), move |settings, key| {
let value = settings.string(key);
let orientation_mode: OrientationMode = value.into();
app_window.imp().orientation_mode.set(orientation_mode);
app_window.imp().set_orientation();
});
}
let app_window = self.obj();
self.unit_list_panel
.register_selection_change(&app_window, &self.refresh_unit_list_button);
self.unit_control_panel.set_app_window(&app_window);
self.setup_dropdown();
let condition_1 = adw::BreakpointCondition::new_ratio(
adw::BreakpointConditionRatioType::MinAspectRatio,
4,
3,
);
let condition_2 = adw::BreakpointCondition::new_ratio(
adw::BreakpointConditionRatioType::MaxAspectRatio,
16,
9,
);
let condition = adw::BreakpointCondition::new_and(condition_1, condition_2);
self.breakpoint.set_condition(Some(&condition));
{
let app_window = self.obj().clone();
self.breakpoint.connect_unapply(move |_breakpoint| {
debug!("connect_unapply");
app_window.imp().set_orientation();
});
}
let menu_views = UnitCuratedList::menu_items();
self.unit_list_view_menubutton
.set_menu_model(Some(&menu_views));
settings
.bind(WINDOW_WIDTH, self.obj().as_ref(), "default-width")
.mapping(move |variant, _| {
let width = variant.get::<i32>();
let w = if let Some(width) = width
&& width > 0
{
width
} else {
DEFAULT_WIDTH
};
Some(w.to_value())
})
.build();
settings
.bind(WINDOW_HEIGHT, self.obj().as_ref(), "default-height")
.mapping(move |variant, _| {
let height = variant.get::<i32>();
let h = if let Some(height) = height
&& height > 0
{
height
} else {
DEFAULT_HEIGHT
};
Some(h.to_value())
})
.build();
settings
.bind(IS_MAXIMIZED, self.obj().as_ref(), "maximized")
.build();
{
let win = self.obj().clone();
settings
.bind::<gtk::Paned>(PANED_SEPARATOR_POSITION, &self.paned, "position")
.mapping(move |variant, _| {
let pane_position = variant.get::<i32>();
let pane_position = if let Some(pane_position) = pane_position
&& pane_position >= 0
{
pane_position
} else {
let (w, h) = win.default_size();
let orientation = win.imp().paned.orientation();
let w = if w < 0 { DEFAULT_WIDTH } else { w };
let h = if h < 0 { DEFAULT_HEIGHT } else { h };
if orientation == gtk::Orientation::Horizontal {
h / 2
} else {
w / 2
}
};
Some(pane_position.to_value())
})
.build();
}
settings
.bind::<gtk::Paned>(WINDOW_PANES_ORIENTATION, &self.paned, "orientation")
.mapping(move |variant, _| {
let window_panes_orientation = variant.get::<String>();
let window_panes_orientation =
if window_panes_orientation.as_deref() == Some(HORIZONTAL) {
gtk::Orientation::Horizontal
} else {
gtk::Orientation::Vertical
};
window_panes_orientation.into_closure_return_value()
})
.set_mapping(move |value, _| {
let orientation = value
.get::<gtk::Orientation>()
.inspect_err(|err| warn!("Conv error {:?}", err))
.unwrap_or(gtk::Orientation::Vertical);
let window_panes_orientation = if orientation == gtk::Orientation::Horizontal {
HORIZONTAL
} else {
VERTICAL
};
Some(window_panes_orientation.to_variant())
})
.build();
}
}
#[gtk::template_callbacks]
impl AppWindowImpl {
fn setup_dropdown(&self) {
let model = adw::EnumListModel::new(DbusLevel::static_type());
let empty: [gtk::Expression; 0] = [];
let expression = gtk::ClosureExpression::new::<String>(
empty,
glib::closure!(|s: adw::EnumListItem| {
let dbus: DbusLevel = s.value().into();
dbus.label()
}),
);
self.system_session_dropdown
.set_expression(Some(expression));
self.system_session_dropdown.set_model(Some(&model));
{
let settings = systemd_gui::new_settings();
let level = PREFERENCES.dbus_level();
self.system_session_dropdown.set_selected(level as u32);
self.system_session_dropdown
.connect_selected_item_notify(move |dropdown| {
let idx = dropdown.selected();
let level: DbusLevel = idx.into();
debug!("System Session Values Selected idx {idx:?} level {level:?}");
PREFERENCES.set_and_save_dbus_level(level, &settings);
if let Err(err) = dropdown.activate_action(ACTION_WIN_REFRESH_UNIT_LIST, None) {
warn!("call action {ACTION_WIN_REFRESH_UNIT_LIST} error: {err}");
}
});
}
}
pub fn save_window_context(&self) -> Result<(), glib::BoolError> {
let obj = self.obj();
let (width, height) = obj.default_size();
let settings = systemd_gui::new_settings();
settings.set_int(WINDOW_WIDTH, width)?;
settings.set_int(WINDOW_HEIGHT, height)?;
settings.set_boolean(IS_MAXIMIZED, obj.is_maximized())?;
let separator_position = self.paned.position();
settings.set_int(PANED_SEPARATOR_POSITION, separator_position)?;
let window_panes_orientation = if self.paned.orientation() == gtk::Orientation::Horizontal {
HORIZONTAL
} else {
VERTICAL
};
settings.set_string(WINDOW_PANES_ORIENTATION, window_panes_orientation)?;
Ok(())
}
#[template_callback]
fn button_search_toggled(&self, toggle_button: >k::ToggleButton) {
self.unit_list_panel
.button_search_toggled(toggle_button.is_active());
}
}
impl AppWindowImpl {
fn set_orientation(&self) {
let window_panes_orientation = self.paned.orientation();
let orientation_mode = self.orientation_mode.get();
let orientation = match orientation_mode {
OrientationMode::Automatic => {
let (width, height) = self.obj().default_size();
let ratio = height as f32 / width as f32;
if ratio >= 3.0 / 4.0 {
gtk::Orientation::Vertical
} else if ratio <= 9.0 / 16.0 {
gtk::Orientation::Horizontal
} else {
window_panes_orientation
}
}
OrientationMode::ForceHorizontal => gtk::Orientation::Horizontal,
OrientationMode::ForceVertical => gtk::Orientation::Vertical,
};
self.paned.set_orientation(orientation);
}
pub(super) fn selection_change(&self, unit: Option<&UnitInfo>) {
if let Some(unit) = unit {
self.app_title.set_subtitle(&unit.primary());
} else {
self.app_title.set_subtitle("");
}
self.selected_unit.replace(unit.cloned());
self.unit_control_panel.selection_change(unit);
}
pub(super) fn set_unit(&self, unit: Option<&UnitInfo>) -> Option<UnitInfo> {
self.selection_change(unit);
self.unit_list_panel.set_unit(unit)
}
pub fn set_inter_message(&self, action: &InterPanelMessage) {
self.unit_control_panel.set_inter_message(action);
self.unit_list_panel.set_inter_message(action);
}
pub(super) fn build_action(&self, application: &adw::Application) {
let search_toggle_button = self.search_toggle_button.clone();
let unit_list_panel = self.unit_list_panel.clone();
let search_units: gio::ActionEntry<adw::Application> =
gio::ActionEntry::builder(&APP_ACTION_SEARCH_UNITS[4..])
.activate(move |_, _, _| {
if !search_toggle_button.is_active() {
search_toggle_button.activate();
} else {
unit_list_panel.button_search_toggled(true);
}
})
.build();
let open_info: gio::ActionEntry<adw::Application> = {
let unit_control_panel = self.unit_control_panel.clone();
gio::ActionEntry::builder("open_info")
.activate(move |_application: &adw::Application, _, _| {
unit_control_panel.display_info_page();
})
.build()
};
let open_dependencies: gio::ActionEntry<adw::Application> = {
let unit_control_panel = self.unit_control_panel.clone();
gio::ActionEntry::builder("open_dependencies")
.activate(move |_application: &adw::Application, _, _| {
unit_control_panel.display_dependencies_page();
})
.build()
};
let open_journal: gio::ActionEntry<adw::Application> = {
let unit_control_panel = self.unit_control_panel.clone();
gio::ActionEntry::builder("open_journal")
.activate(move |_application: &adw::Application, _, _| {
unit_control_panel.display_journal_page();
})
.build()
};
let open_file: gio::ActionEntry<adw::Application> = {
let unit_control_panel = self.unit_control_panel.clone();
gio::ActionEntry::builder("open_file")
.activate(move |_application: &adw::Application, _, _| {
unit_control_panel.display_definition_file_page();
})
.build()
};
let default_state = glib::variant::ToVariant::to_variant(&"auto");
let orientation_mode: gio::ActionEntry<adw::Application> =
gio::ActionEntry::builder(KEY_PREF_ORIENTATION_MODE)
.activate(move |_win: &adw::Application, action, variant| {
warn!("action {action:?} variant {variant:?}");
})
.parameter_type(Some(VariantTy::STRING))
.state(default_state)
.build();
let list_boots = {
let app_window = self.obj().clone();
gio::ActionEntry::builder(ACTION_LIST_BOOT)
.activate(move |_, _, _| {
let list_boots_window = ListBootsWindow::new(&app_window);
list_boots_window.set_modal(false);
list_boots_window.present();
})
.build()
};
let create_unit = {
let app_window = self.obj().clone();
gio::ActionEntry::builder(&ACTION_APP_CREATE_UNIT[4..])
.activate(move |_, _action, _variant| {
let list_boots_window = UnitCreatorWindow::new(&app_window);
list_boots_window.set_modal(false);
list_boots_window.present();
})
.build()
};
let properties_selector = {
let app_window = self.obj().clone();
let unit_list_panel = self.unit_list_panel.clone();
gio::ActionEntry::builder(ACTION_PROPERTIES_SELECTOR)
.activate(move |_, _action, variant| {
let column_id = variant.map(|v| v.get::<String>().unwrap());
let dialog = UnitPropertiesSelectorDialog::new(&unit_list_panel, column_id);
dialog.set_transient_for(Some(&app_window));
dialog.present();
})
.parameter_type(Some(VariantTy::STRING))
.build()
};
let properties_selector_general = {
let app_window = self.obj().clone();
let unit_list_panel = self.unit_list_panel.clone();
gio::ActionEntry::builder(ACTION_PROPERTIES_SELECTOR_GENERAL)
.activate(move |_, _action, variant| {
let column_id = variant.map(|v| v.get::<String>().unwrap());
let dialog = UnitPropertiesSelectorDialog::new(&unit_list_panel, column_id);
dialog.set_transient_for(Some(&app_window));
dialog.present();
})
.build()
};
let print_debug = {
let unit_list_panel = self.unit_list_panel.clone();
gio::ActionEntry::builder("debug")
.activate(move |_, _action, _variant| {
unit_list_panel.print_scroll_adj_logs();
})
.build()
};
let display_unit_properties = {
let app_window = self.obj().clone();
gio::ActionEntry::builder(ACTION_UNIT_PROPERTIES_DISPLAY)
.activate(move |_, _action, _variant| {
let Some(selected_unit) = app_window.selected_unit() else {
warn!("Can't display unit properties, No unit selected");
return;
};
info!(
"Displaying unit properties for {:?}",
selected_unit.primary()
);
let unit_properties = InfoWindow::new(Some(&selected_unit));
unit_properties.set_transient_for(Some(&app_window));
unit_properties.present();
})
.build()
};
const ACTION_APP_QUIT: &str = "app.quit";
let quit = gio::ActionEntry::builder(&ACTION_APP_QUIT[4..])
.activate(move |app: &adw::Application, _, _| app.quit())
.build();
application.add_action_entries([
search_units,
open_info,
open_dependencies,
open_journal,
open_file,
orientation_mode,
list_boots,
properties_selector,
properties_selector_general,
print_debug,
display_unit_properties,
create_unit,
quit,
]);
application.set_accels_for_action(APP_ACTION_SEARCH_UNITS, &["<Ctrl>f"]);
application.set_accels_for_action("app.open_info", &["<Ctrl>i"]);
application.set_accels_for_action("app.open_dependencies", &["<Ctrl>d"]);
application.set_accels_for_action("app.open_journal", &["<Ctrl>j"]);
application.set_accels_for_action("app.open_file", &["<Ctrl>u"]);
application.set_accels_for_action("win.unit_list_filter_blank", &["<Ctrl><Alt>f"]);
application.set_accels_for_action(APP_ACTION_LIST_BOOT, &["<Ctrl>b"]);
application.set_accels_for_action("app.signals", &["<Ctrl>g"]);
application.set_accels_for_action(APP_ACTION_PROPERTIES_SELECTOR_GENERAL, &["<Ctrl>l"]);
application.set_accels_for_action(ACTION_APP_QUIT, &["<Ctrl>q"]);
application.set_accels_for_action("app.debug", &["<Ctrl>1"]);
application.set_accels_for_action(APP_ACTION_UNIT_PROPERTIES_DISPLAY, &["<Ctrl>p"]);
application.set_accels_for_action(WIN_ACTION_SAVE_UNIT_FILE, &["<Ctrl>s"]);
application.set_accels_for_action(ACTION_DAEMON_RELOAD, &["<Ctrl>r"]);
application.set_accels_for_action(ACTION_FIND_IN_TEXT_OPEN, &["<Shift><Ctrl>f"]);
application.set_accels_for_action(ACTION_APP_CREATE_UNIT, &["<Shift><Ctrl>c"]);
application.set_accels_for_action("win.close", &["<Ctrl>w"]);
for ui in UnitCuratedList::iter() {
application.set_accels_for_action(&ui.detailed_action(), &ui.win_accels());
}
}
pub fn overlay(&self) -> &adw::ToastOverlay {
&self.toast_overlay
}
pub(super) fn add_toast_message(
&self,
message: &str,
use_markup: bool,
action: Option<(&str, String, bool)>,
) {
let msg = if use_markup {
let out = replace_tags(message);
Cow::from(out)
} else {
Cow::from(message)
};
let toast = adw::Toast::builder()
.title(msg)
.use_markup(use_markup)
.build();
if let Some((action_name, ref button_label, user_session)) = action {
info!("Toast action {:?} user_session {user_session}", action);
toast.set_action_name(Some(action_name));
toast.set_action_target_value(Some(&user_session.to_variant()));
toast.set_button_label(Some(button_label));
}
self.toast_overlay.add_toast(toast)
}
pub fn update_list_boots(&self, boots: Vec<Rc<Boot>>) {
self.list_boots.replace(Some(boots));
}
pub fn cached_list_boots(&self) -> Ref<'_, Option<Vec<Rc<Boot>>>> {
self.list_boots.borrow()
}
pub fn cached_list_boots_mut(&self) -> RefMut<'_, Option<Vec<Rc<Boot>>>> {
self.list_boots.borrow_mut()
}
}
impl WidgetImpl for AppWindowImpl {}
impl WindowImpl for AppWindowImpl {
fn close_request(&self) -> glib::Propagation {
#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
if let Err(err) = systemd::shut_down_sysd_proxy() {
warn!("Closing Proxy Error {err:?}");
}
debug!("Close window");
self.unit_list_panel.save_column_config();
self.parent_close_request();
glib::Propagation::Proceed
}
}
impl AdwApplicationWindowImpl for AppWindowImpl {}
impl ApplicationWindowImpl for AppWindowImpl {}
#[cfg(test)]
mod tests {
use crate::widget::toast_regex;
#[test]
fn test_reg_ex1() {
let r = toast_regex();
let test_str = "asdf <unit>unit.serv</unit> ok";
for capt in r.captures_iter(test_str) {
println!("capture: {capt:#?}")
}
}
#[test]
fn test_reg_ex2() {
let r = toast_regex();
let test_str = [
"asdf <unit arg=\"test\">unit.serv</unit> ok",
"Clean unit <unit>tiny_daemon.service</unit> with parameters <b>cache</b> and <b>configuration</b> failed",
];
for test in test_str {
for capt in r.captures_iter(test) {
println!("capture: {capt:#?}")
}
}
}
}