use std::cell::{Cell, OnceCell, RefCell};
use adw::{prelude::*, subclass::window::AdwWindowImpl};
use gettextrs::pgettext;
use gtk::{
TemplateChild,
glib::{self, property::PropertySet},
subclass::{
prelude::*,
widget::{
CompositeTemplateCallbacksClass, CompositeTemplateClass,
CompositeTemplateInitializingExt, WidgetClassExt, WidgetImpl,
},
},
};
use crate::{
consts::{ERROR_CSS, WARNING_CSS},
format2,
systemd::{self, data::UnitInfo, enums::KillWho, errors::SystemdErrors},
widget::{
InterPanelMessage,
unit_control_panel::{UnitControlPanel, side_control_panel::SideControlPanel},
},
};
use base::enums::UnitDBusLevel;
use tracing::{debug, warn};
#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/kill_panel.ui")]
pub struct KillPanelImp {
#[template_child]
send_button: TemplateChild<gtk::Button>,
#[template_child]
signal_id_entry: TemplateChild<adw::EntryRow>,
#[template_child]
who_to_kill: TemplateChild<adw::ComboRow>,
#[template_child]
sigqueue_value: TemplateChild<adw::EntryRow>,
#[template_child]
window_title: TemplateChild<adw::WindowTitle>,
#[template_child]
signals_box: TemplateChild<gtk::Box>,
unit: RefCell<Option<UnitInfo>>,
is_sigqueue: Cell<bool>,
parent: OnceCell<SideControlPanel>,
}
#[gtk::template_callbacks]
impl KillPanelImp {
#[template_callback]
fn button_send_clicked(&self, button: >k::Button) {
self.button_send_clicked2(button);
}
pub(super) fn parent(&self) -> &SideControlPanel {
self.parent.get().expect("Parent not supposed to be None")
}
pub fn set_unit(&self, unit: Option<&UnitInfo>) {
let unit = match unit {
Some(u) => u,
None => {
warn!("set unit to None");
self.unit.set(None);
let sub_title = pgettext("kill", "No Unit Selected");
self.window_title.set_subtitle(&sub_title);
return;
}
};
self.unit.set(Some(unit.clone()));
let label_text = &unit.primary();
self.window_title.set_subtitle(label_text);
self.set_send_button_sensitivity();
}
#[template_callback]
fn kill_signal_text_change(&self, entry: &adw::EntryRow) {
self.set_send_button_sensitivity();
Self::validate_entry(entry, self.is_sigqueue.get(), true)
}
#[template_callback]
fn sigqueue_value_changed(&self, entry: &adw::EntryRow) {
self.set_send_button_sensitivity();
Self::validate_entry(entry, self.is_sigqueue.get(), false)
}
fn validate_entry(entry: &adw::EntryRow, is_sigqueue: bool, is_signal_entry: bool) {
let value_txt = entry.text();
if value_txt.is_empty() {
entry.remove_css_class(WARNING_CSS);
entry.remove_css_class(ERROR_CSS);
} else {
match value_txt.parse::<i32>() {
Ok(value) => {
entry.remove_css_class(ERROR_CSS);
if is_signal_entry && is_sigqueue {
if (libc::SIGRTMIN()..=libc::SIGRTMAX()).contains(&value) {
entry.remove_css_class(WARNING_CSS);
} else {
entry.add_css_class(WARNING_CSS);
}
}
}
Err(parse_int_error) => match parse_int_error.kind() {
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow => {
entry.remove_css_class(ERROR_CSS);
entry.add_css_class(WARNING_CSS);
}
_ => {
entry.add_css_class(ERROR_CSS);
entry.remove_css_class(WARNING_CSS);
}
},
}
}
}
#[template_callback]
fn who_to_kill_activate(&self, combo_row: &adw::ComboRow) {
debug!("who_to_kill_activate {}", combo_row.index());
}
#[template_callback]
fn who_to_kill_activated(&self, combo_row: &adw::ComboRow) {
debug!("who_to_kill_activated {}", combo_row.index());
}
fn contruct_signals_description(&self, sg: Signal) {
let title = sg.name;
let subtitle = format2!(
pgettext("kill", "{}\nDefault Action: {}"),
sg.comment,
sg.default_action
);
let action_row = adw::ActionRow::builder()
.title(title)
.subtitle(&subtitle)
.subtitle_lines(2)
.margin_end(5)
.margin_start(5)
.build();
let button_label = sg.id.to_string();
let action_button = gtk::Button::builder()
.label(&button_label)
.css_classes(["circular", "raised"])
.valign(gtk::Align::BaselineCenter)
.build();
let entry_row = self.signal_id_entry.clone();
action_button.connect_clicked(move |_| {
entry_row.set_text(&button_label);
});
action_row.add_suffix(&action_button);
self.signals_box.append(&action_row);
}
}
impl KillPanelImp {
pub(super) fn set_inter_message(&self, action: &InterPanelMessage) {
if let InterPanelMessage::UnitChange(unit) = *action {
self.set_unit(unit)
}
}
pub(super) fn set_is_signal(&self, is_signal: bool) {
self.is_sigqueue.set(is_signal);
if is_signal {
let title = pgettext("kill", "Queue a Realtime Signal to Unit");
self.window_title.set_title(&title);
self.sigqueue_value.set_visible(true);
let min = libc::SIGRTMIN();
let max = libc::SIGRTMAX();
let span = max - min;
let span_d2 = span / 2;
for id in min..=max {
let offset = id - min;
let name = if offset == 0 {
"SIGRTMIN".to_string()
} else if offset == span {
"SIGRTMAX".to_string()
} else if offset > span_d2 {
format!("SIGRTMAX-{}", span - offset)
} else {
format!("SIGRTMIN+{offset}")
};
let signal = Signal {
id: (id as u32),
name: &name,
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Real-time signal"),
};
self.contruct_signals_description(signal);
}
} else {
let title = pgettext("kill", "Send a Kill Signal to Unit");
self.window_title.set_title(&title);
self.sigqueue_value.set_visible(false);
for signal in signals() {
self.contruct_signals_description(signal);
}
}
}
fn set_send_button_sensitivity(&self) {
let text = self.signal_id_entry.text();
let button_sensitive = match (
text.is_empty(),
text.contains(pattern_not_digit),
self.unit.borrow().is_some(),
self.is_sigqueue.get(),
) {
(false, false, true, false) => true,
(false, false, true, true) => {
let signal_value_text = self.sigqueue_value.text();
matches!(
(
signal_value_text.is_empty(),
signal_value_text.contains(pattern_not_digit)
),
(false, false)
)
}
_a => {
debug!("a {_a:?}");
false
}
};
self.send_button.set_sensitive(button_sensitive);
}
pub(crate) fn set_parent(&self, parent: &SideControlPanel) {
self.parent
.set(parent.clone())
.expect("parent should be set once");
}
fn button_send_clicked2(&self, button: >k::Button) {
let text = self.signal_id_entry.text();
let Ok(signal_id) = text.parse::<i32>() else {
warn!("Kill/Queued signal id not a number");
return;
};
let who: KillWho = self.who_to_kill.selected().into();
let lambda_out = move |_method_name: &str,
_unit: Option<&UnitInfo>,
_res: Result<(), SystemdErrors>,
_control: &UnitControlPanel| {};
if self.is_sigqueue.get() {
let sigqueue_value = match self.sigqueue_value.text().parse::<i32>() {
Ok(v) => v,
Err(err) => {
warn!("Queued Signal value not a number: {err:?}");
0
}
};
let prefix = format2!(
pgettext("kill", "Queued signal {} with value {} to"),
signal_id,
sigqueue_value
);
let lambda = move |params: Option<(UnitDBusLevel, String)>| {
if let Some((level, primary_name)) = params {
systemd::queue_signal_unit(level, &primary_name, who, signal_id, sigqueue_value)
} else {
Err(SystemdErrors::NoUnit)
}
};
self.parent()
.call_method(&prefix, true, button, lambda, lambda_out);
} else {
let prefix = format2!(pgettext("kill", "Kill signal {} to"), signal_id);
let lambda = move |params: Option<(UnitDBusLevel, String)>| {
if let Some((level, primary_name)) = params {
systemd::kill_unit(level, &primary_name, who, signal_id)
} else {
Err(SystemdErrors::NoUnit)
}
};
self.parent()
.call_method(&prefix, true, button, lambda, lambda_out);
}
}
}
#[glib::object_subclass]
impl ObjectSubclass for KillPanelImp {
const NAME: &'static str = "KillPanel";
type Type = super::KillPanel;
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();
}
}
impl ObjectImpl for KillPanelImp {
fn constructed(&self) {
self.parent_constructed();
let expression = gtk::PropertyExpression::new(
adw::EnumListItem::static_type(),
None::<gtk::Expression>,
"nick",
);
self.who_to_kill.set_expression(Some(expression));
let model = adw::EnumListModel::new(KillWho::static_type());
self.who_to_kill.set_model(Some(&model));
self.who_to_kill
.connect_selected_item_notify(|who_to_kill| {
let o = who_to_kill.selected_item();
let Some(object) = o else {
return;
};
let item = object
.downcast_ref::<adw::EnumListItem>()
.expect("Suppose to be a EnumListItem");
let kill_who: KillWho = item.value().into();
who_to_kill.set_subtitle(kill_who.description());
});
let edit = self.signal_id_entry.delegate().unwrap();
gtk::Editable::connect_insert_text(&edit, move |entry, text, position| {
if text.contains(pattern_not_digit) {
glib::signal::signal_stop_emission_by_name(entry, "insert-text");
entry.insert_text(&text.replace(pattern_not_digit, ""), position);
}
});
}
}
impl WidgetImpl for KillPanelImp {}
impl WindowImpl for KillPanelImp {
fn close_request(&self) -> glib::Propagation {
self.parent_close_request();
self.unit.set(None);
if let Some(parent) = self.parent.get() {
parent.unlink_child(self.is_sigqueue.get());
}
glib::Propagation::Proceed
}
}
impl AdwWindowImpl for KillPanelImp {}
fn pattern_not_digit(c: char) -> bool {
!c.is_ascii_digit()
}
struct Signal<'a> {
id: u32,
name: &'a str,
default_action: String,
comment: String,
}
fn signals<'a>() -> [Signal<'a>; 34] {
[
Signal {
id: 1,
name: "SIGHUP",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Hang up controlling terminal or process"),
},
Signal {
id: 2,
name: "SIGINT",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Interrupt from keyboard, Ctrl+C"),
},
Signal {
id: 3,
name: "SIGQUIT",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Quit from keyboard, Ctrl+\\"),
},
Signal {
id: 4,
name: "SIGILL",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Illegal instruction"),
},
Signal {
id: 5,
name: "SIGTRAP",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Breakpoint for debugging"),
},
Signal {
id: 6,
name: "SIGABRT",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Abnormal termination"),
},
Signal {
id: 6,
name: "SIGIOT",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Equivalent to SIGABRT"),
},
Signal {
id: 7,
name: "SIGBUS",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Bus error"),
},
Signal {
id: 8,
name: "SIGFPE",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Floating-point exception"),
},
Signal {
id: 9,
name: "SIGKILL",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Forced-process termination"),
},
Signal {
id: 10,
name: "SIGUSR1",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Available to processes"),
},
Signal {
id: 11,
name: "SIGSEGV",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Invalid memory reference"),
},
Signal {
id: 12,
name: "SIGUSR2",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Available to processes"),
},
Signal {
id: 13,
name: "SIGPIPE",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Write to pipe with no readers"),
},
Signal {
id: 14,
name: "SIGALRM",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Real-timer clock"),
},
Signal {
id: 15,
name: "SIGTERM",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Process termination"),
},
Signal {
id: 16,
name: "SIGSTKFLT",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Coprocessor stack error"),
},
Signal {
id: 17,
name: "SIGCHLD",
default_action: pgettext("kill", "Ignore"),
comment: pgettext(
"kill",
"Child process stopped or terminated or got a signal if traced",
),
},
Signal {
id: 18,
name: "SIGCONT",
default_action: pgettext("kill", "Continue"),
comment: pgettext("kill", "Resume execution, if stopped"),
},
Signal {
id: 19,
name: "SIGSTOP",
default_action: pgettext("kill", "Stop"),
comment: pgettext("kill", "Stop process execution, Ctrl+Z"),
},
Signal {
id: 20,
name: "SIGTSTP",
default_action: pgettext("kill", "Stop"),
comment: pgettext("kill", "Stop process issued from tty"),
},
Signal {
id: 21,
name: "SIGTTIN",
default_action: pgettext("kill", "Stop"),
comment: pgettext("kill", "Background process requires input"),
},
Signal {
id: 22,
name: "SIGTTOU",
default_action: pgettext("kill", "Stop"),
comment: pgettext("kill", "Background process requires output"),
},
Signal {
id: 23,
name: "SIGURG",
default_action: pgettext("kill", "Ignore"),
comment: pgettext("kill", "Urgent condition on socket"),
},
Signal {
id: 24,
name: "SIGXCPU",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "CPU time limit exceeded"),
},
Signal {
id: 25,
name: "SIGXFSZ",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "File size limit exceeded"),
},
Signal {
id: 26,
name: "SIGVTALRM",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Virtual timer clock"),
},
Signal {
id: 27,
name: "SIGPROF",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Profile timer clock"),
},
Signal {
id: 28,
name: "SIGWINCH",
default_action: pgettext("kill", "Ignore"),
comment: pgettext("kill", "Window resizing"),
},
Signal {
id: 29,
name: "SIGIO",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "I/O now possible"),
},
Signal {
id: 29,
name: "SIGPOLL",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Equivalent to SIGIO"),
},
Signal {
id: 30,
name: "SIGPWR",
default_action: pgettext("kill", "Terminate"),
comment: pgettext("kill", "Power supply failure"),
},
Signal {
id: 31,
name: "SIGSYS",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Bad system call"),
},
Signal {
id: 31,
name: "SIGUNUSED",
default_action: pgettext("kill", "Dump"),
comment: pgettext("kill", "Equivalent to SIGSYS"),
},
]
}