#![crate_type = "bin"]
use sysinfo::*;
use gtk::gdk::Texture;
use gtk::gdk_pixbuf::Pixbuf;
use gtk::gio::prelude::*;
use gtk::gio::MemoryInputStream;
use gtk::glib::{Bytes, IsA, ToVariant};
use gtk::prelude::*;
use gtk::{gdk, gio, glib};
use gtk::{AboutDialog, Dialog, Entry, MessageDialog};
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
mod color;
mod display_disk;
#[macro_use]
mod display_sysinfo;
mod display_network;
mod display_procs;
mod graph;
mod network_dialog;
mod notebook;
mod process_dialog;
mod settings;
mod utils;
use display_network::Network;
use display_procs::{create_and_fill_model, Procs};
use display_sysinfo::DisplaySysInfo;
use settings::Settings;
use utils::format_number;
pub const APPLICATION_NAME: &str = "fr.guillaume_gomez.ProcessViewer";
fn update_window(list: >k::ListStore, entries: &HashMap<Pid, sysinfo::Process>) {
let mut seen: HashSet<Pid> = HashSet::new();
if let Some(iter) = list.iter_first() {
let mut valid = true;
while valid {
let pid = match list.get_value(&iter, 0).get::<u32>() {
Ok(pid) => Pid::from_u32(pid),
_ => {
valid = list.iter_next(&iter);
continue;
}
};
if let Some(p) = entries.get(&(pid)) {
let disk_usage = p.disk_usage();
let disk_usage = disk_usage.written_bytes + disk_usage.read_bytes;
let memory = p.memory() * 1_000;
list.set(
&iter,
&[
(2, &format!("{:.1}", p.cpu_usage())),
(3, &format_number(memory)),
(
4,
&if disk_usage > 0 {
format_number(disk_usage)
} else {
String::new()
},
),
(6, &p.cpu_usage()),
(7, &memory),
(8, &disk_usage),
],
);
valid = list.iter_next(&iter);
seen.insert(pid);
} else {
valid = list.remove(&iter);
}
}
}
for (pid, pro) in entries.iter() {
if !seen.contains(pid) {
create_and_fill_model(
list,
pid.as_u32(),
pro.cmd(),
pro.name(),
pro.cpu_usage(),
pro.memory() * 1_000,
);
}
}
}
fn parse_quote(line: &str, quote: char) -> Vec<String> {
let args = line.split(quote).collect::<Vec<&str>>();
let mut out_args = vec![];
for (num, arg) in args.iter().enumerate() {
if num != 1 {
out_args.extend_from_slice(&parse_entry(arg));
} else {
out_args.push((*arg).to_owned());
}
}
out_args
}
fn parse_entry(line: &str) -> Vec<String> {
match (line.find('\''), line.find('"')) {
(Some(x), Some(y)) => {
if x < y {
parse_quote(line, '\'')
} else {
parse_quote(line, '"')
}
}
(Some(_), None) => parse_quote(line, '\''),
(None, Some(_)) => parse_quote(line, '"'),
(None, None) => line
.split(' ')
.map(::std::borrow::ToOwned::to_owned)
.collect::<Vec<String>>(),
}
}
#[cfg(unix)]
fn build_command(c: &mut Command) -> &mut Command {
unsafe {
c.pre_exec(|| {
libc::setsid();
Ok(())
})
}
}
#[cfg(windows)]
fn build_command(c: &mut Command) -> &mut Command {
c
}
fn start_detached_process(line: &str) -> Option<String> {
let args = parse_entry(line);
let command = args[0].clone();
let cmd = build_command(Command::new(&command).args(&args))
.stdin(Stdio::null())
.stderr(Stdio::null())
.stdout(Stdio::null())
.spawn();
if cmd.is_err() {
Some(format!("Failed to start '{}'", &command))
} else {
None
}
}
fn run_command<T: IsA<gtk::Window>>(input: &Entry, window: &T, d: &Dialog) {
let text = input.text();
let x = if let Some(x) = start_detached_process(&text) {
x
} else {
"The command started successfully".to_owned()
};
d.close();
let m = MessageDialog::new(
Some(window),
gtk::DialogFlags::DESTROY_WITH_PARENT,
gtk::MessageType::Info,
gtk::ButtonsType::Ok,
&x,
);
m.set_modal(true);
m.connect_response(|dialog, response| {
if response == gtk::ResponseType::DeleteEvent
|| response == gtk::ResponseType::Close
|| response == gtk::ResponseType::Ok
{
dialog.close();
}
});
m.show();
}
fn create_new_proc_diag(
process_dialogs: &Rc<RefCell<Vec<process_dialog::ProcDialog>>>,
pid: Pid,
sys: &sysinfo::System,
) {
if let Some(proc_diag) = process_dialogs
.borrow()
.iter()
.filter(|x| !x.is_dead)
.find(|x| x.pid == pid)
{
proc_diag.popup.present();
return;
}
let total_memory = sys.total_memory();
if let Some(process) = sys.process(pid) {
process_dialogs
.borrow_mut()
.push(process_dialog::create_process_dialog(process, total_memory));
}
}
#[derive(Clone)]
pub struct RequiredForSettings {
process_refresh_timeout: Arc<Mutex<u32>>,
network_refresh_timeout: Arc<Mutex<u32>>,
system_refresh_timeout: Arc<Mutex<u32>>,
sys: Arc<Mutex<sysinfo::System>>,
process_dialogs: Rc<RefCell<Vec<process_dialog::ProcDialog>>>,
list_store: gtk::ListStore,
display_tab: Rc<RefCell<DisplaySysInfo>>,
network_tab: Rc<RefCell<Network>>,
}
fn setup_timeout(rfs: &RequiredForSettings) {
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let sys = &rfs.sys;
let process_dialogs = &rfs.process_dialogs;
let list_store = &rfs.list_store;
let process_refresh_timeout = &rfs.process_refresh_timeout;
thread::spawn(
glib::clone!(@weak sys, @weak process_refresh_timeout => move || {
loop {
let sleep_dur = Duration::from_millis(
*process_refresh_timeout.lock().expect("failed to lock process refresh mutex") as _);
thread::sleep(sleep_dur);
sys.lock().expect("failed to lock to refresh processes").refresh_processes();
ready_tx.send(false).expect("failed to send data through process refresh channel");
}
}),
);
ready_rx.attach(None,
glib::clone!(@weak sys, @weak list_store, @weak process_dialogs => @default-return glib::Continue(true), move |_: bool| {
let sorted = TreeSortableExtManual::sort_column_id(&list_store);
list_store.set_unsorted();
let mut dialogs = process_dialogs.borrow_mut();
if let Ok(sys) = sys.lock() {
update_window(&list_store, sys.processes());
if let Some((col, order)) = sorted {
list_store.set_sort_column_id(col, order);
}
for dialog in dialogs.iter_mut().filter(|x| !x.is_dead) {
if let Some(process) = sys.processes().get(&dialog.pid) {
dialog.update(process);
} else {
dialog.set_dead();
}
}
} else {
panic!("failed to lock sys to refresh UI");
}
dialogs.retain(|x| !x.need_remove());
glib::Continue(true)
}));
}
fn setup_network_timeout(rfs: &RequiredForSettings) {
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let network_refresh_timeout = &rfs.network_refresh_timeout;
let network_tab = &rfs.network_tab;
let sys = &rfs.sys;
thread::spawn(
glib::clone!(@weak sys, @weak network_refresh_timeout => move || {
loop {
let sleep_dur = Duration::from_millis(
*network_refresh_timeout.lock().expect("failed to lock networks refresh mutex") as _);
thread::sleep(sleep_dur);
sys.lock().expect("failed to lock to refresh networks").refresh_networks();
ready_tx.send(false).expect("failed to send data through networks refresh channel");
}
}),
);
ready_rx.attach(None,
glib::clone!(@weak sys, @weak network_tab => @default-panic, move |_: bool| {
network_tab.borrow_mut().update_networks(&*sys.lock().expect("failed to lock to update networks"));
glib::Continue(true)
})
);
}
fn setup_system_timeout(rfs: &RequiredForSettings, settings: &Rc<RefCell<Settings>>) {
let (ready_tx, ready_rx) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
let system_refresh_timeout = &rfs.system_refresh_timeout;
let sys = &rfs.sys;
let display_tab = &rfs.display_tab;
thread::spawn(
glib::clone!(@weak sys, @weak system_refresh_timeout => move || {
loop {
let sleep_dur = Duration::from_millis(
*system_refresh_timeout.lock().expect("failed to lock system refresh mutex") as _);
thread::sleep(sleep_dur);
sys.lock().expect("failed to lock to refresh system").refresh_system();
ready_tx.send(false).expect("failed to send data through system refresh channel");
}
}),
);
ready_rx.attach(
None,
glib::clone!(@weak sys, @weak display_tab, @weak settings => @default-panic, move |_: bool| {
let mut info = display_tab.borrow_mut();
let sys = sys.lock().expect("failed to lock to update system");
let display_fahrenheit = settings.borrow().display_fahrenheit;
info.update_system_info(&*sys, display_fahrenheit);
info.update_system_info_display(&*sys);
glib::Continue(true)
}),
);
}
fn create_header_bar(stack: >k::Stack) -> (gtk::HeaderBar, gtk::Button) {
let header_buttons = gtk::StackSwitcher::new();
header_buttons.set_stack(Some(stack));
let menu_bar = gio::Menu::new();
menu_bar.append(Some("Launch new executable"), Some("app.new-task"));
let settings_menu = gio::Menu::new();
settings_menu.append(Some("Display temperature in °F"), Some("app.temperature"));
settings_menu.append(Some("Display graphs"), Some("app.graphs"));
settings_menu.append(Some("More settings..."), Some("app.settings"));
menu_bar.append_section(None, &settings_menu);
let more_menu = gio::Menu::new();
more_menu.append(Some("About"), Some("app.about"));
menu_bar.append_section(None, &more_menu);
let menu = gio::Menu::new();
menu.append(Some("Quit"), Some("app.quit"));
menu_bar.append_section(None, &menu);
let menu_button = gtk::MenuButton::builder()
.icon_name("open-menu-symbolic")
.menu_model(&menu_bar)
.build();
menu_button.add_css_class("titlebar-button");
let search_filter_button = gtk::Button::from_icon_name("edit-find-symbolic");
search_filter_button.add_css_class("titlebar-button");
let header_bar = gtk::HeaderBar::new();
header_bar.pack_end(&menu_button);
header_bar.pack_end(&search_filter_button);
header_bar.set_title_widget(Some(&header_buttons));
(header_bar, search_filter_button)
}
fn build_ui(application: >k::Application) {
let window = gtk::ApplicationWindow::new(application);
let stack = gtk::Stack::new();
let (header_bar, search_filter_button) = create_header_bar(&stack);
window.set_titlebar(Some(&header_bar));
let mut sys =
sysinfo::System::new_with_specifics(RefreshKind::everything().without_users_list());
let procs = Procs::new(sys.processes(), &stack);
let current_pid = Rc::clone(&procs.current_pid);
let info_button = procs.info_button.clone();
window.set_default_size(630, 700);
sys.refresh_all();
let sys = Arc::new(Mutex::new(sys));
procs
.kill_button
.connect_clicked(glib::clone!(@weak current_pid, @weak sys => move |_| {
let sys = sys.lock().expect("failed to lock to kill a process");
if let Some(process) = current_pid.get().and_then(|pid| sys.process(pid)) {
process.kill();
}
}));
let settings = Settings::load();
let display_tab = DisplaySysInfo::new(&sys, &stack, &settings);
let settings = Rc::new(RefCell::new(settings));
let network_tab = Rc::new(RefCell::new(Network::new(&stack, &sys)));
display_disk::create_disk_info(&sys, &stack);
let display_tab = Rc::new(RefCell::new(display_tab));
window.set_child(Some(&stack));
let process_dialogs: Rc<RefCell<Vec<process_dialog::ProcDialog>>> =
Rc::new(RefCell::new(Vec::new()));
let list_store = procs.list_store.clone();
let rfs = RequiredForSettings {
process_refresh_timeout: Arc::new(Mutex::new(settings.borrow().refresh_processes_rate)),
network_refresh_timeout: Arc::new(Mutex::new(settings.borrow().refresh_network_rate)),
system_refresh_timeout: Arc::new(Mutex::new(settings.borrow().refresh_system_rate)),
sys: sys.clone(),
process_dialogs: process_dialogs.clone(),
list_store,
display_tab,
network_tab: network_tab.clone(),
};
setup_timeout(&rfs);
setup_network_timeout(&rfs);
setup_system_timeout(&rfs, &settings);
let settings_action = gio::SimpleAction::new("settings", None);
settings_action.connect_activate(glib::clone!(@weak settings, @strong rfs => move |_, _| {
settings::show_settings_dialog(&settings, &rfs);
}));
info_button.connect_clicked(
glib::clone!(@weak current_pid, @weak process_dialogs, @weak sys => move |_| {
if let Some(pid) = current_pid.get() {
create_new_proc_diag(
&process_dialogs,
pid,
&*sys.lock().expect("failed to lock to create new proc dialog"),
);
}
}),
);
procs
.left_tree
.connect_row_activated(glib::clone!(@weak sys => move |tree_view, path, _| {
let model = tree_view.model().expect("couldn't get model");
let iter = model.iter(path).expect("couldn't get iter");
let pid = model.get_value(&iter, 0)
.get::<u32>()
.expect("Model::get failed");
create_new_proc_diag(
&process_dialogs,
Pid::from_u32(pid),
&*sys.lock().expect("failed to lock to create new proc dialog (from tree)"),
);
}
));
let about = gio::SimpleAction::new("about", None);
about.connect_activate(glib::clone!(@weak window => move |_, _| {
let p = AboutDialog::builder()
.authors(vec!["Guillaume Gomez".to_owned()])
.website_label("my website")
.website("https://guillaume-gomez.fr/")
.comments("A process viewer GUI written with gtk-rs")
.copyright("Licensed under MIT")
.program_name("process-viewer")
.transient_for(&window)
.modal(true);
let bytes = Bytes::from_static(include_bytes!(
concat!(env!("CARGO_MANIFEST_DIR"), "/assets/eye.png")));
let memory_stream = MemoryInputStream::from_bytes(&bytes);
let pixbuf = Pixbuf::from_stream(&memory_stream, None::<&gio::Cancellable>);
let p = if let Ok(pixbuf) = pixbuf {
let logo = Texture::for_pixbuf(&pixbuf);
p.logo(&logo)
} else {
p
};
p.build().show();
}));
let new_task = gio::SimpleAction::new("new-task", None);
new_task.connect_activate(glib::clone!(@weak window => move |_, _| {
let dialog = gtk::Dialog::with_buttons(
Some("Launch new executable"),
Some(&window),
gtk::DialogFlags::MODAL,
&[("Run", gtk::ResponseType::Other(0)), ("Cancel", gtk::ResponseType::Close)],
);
let input = Entry::builder()
.css_classes(vec!["button-with-margin".to_owned()])
.vexpand(false)
.hexpand(true)
.build();
dialog.set_response_sensitive(gtk::ResponseType::Other(0), false);
input.connect_changed(glib::clone!(@weak dialog => move |input| {
if !input.text().is_empty() {
dialog.set_response_sensitive(gtk::ResponseType::Other(0), true);
} else {
dialog.set_response_sensitive(gtk::ResponseType::Other(0), false);
}
}));
input.connect_activate(glib::clone!(@weak window, @weak dialog => move |input| {
run_command(input, &window, &dialog);
}));
dialog.connect_response(glib::clone!(@weak input, @weak window => move |dialog, response| {
match response {
gtk::ResponseType::Close => {
dialog.close();
}
gtk::ResponseType::Other(0) => {
run_command(&input, &window, dialog);
}
_ => {}
}
}));
dialog.content_area().append(&input);
dialog.set_size_request(400, 70);
dialog.show();
}));
let graphs = gio::SimpleAction::new_stateful(
"graphs",
None,
&settings.borrow().display_graph.to_variant(),
);
graphs.connect_activate(glib::clone!(@weak settings => move |g, _| {
let mut is_active = false;
if let Some(g) = g.state() {
is_active = g.get().expect("couldn't get bool");
rfs.display_tab.borrow().set_checkboxes_state(!is_active);
}
g.change_state(&(!is_active).to_variant());
settings.borrow_mut().display_graph = !is_active;
settings.borrow().save();
}));
let temperature = gio::SimpleAction::new_stateful(
"temperature",
None,
&settings.borrow().display_fahrenheit.to_variant(),
);
temperature.connect_activate(move |g, _| {
let mut is_active = false;
if let Some(g) = g.state() {
is_active = g.get().expect("couldn't get graph state");
}
g.change_state(&(!is_active).to_variant());
settings.borrow_mut().display_fahrenheit = !is_active;
settings.borrow().save();
});
let quit = gio::SimpleAction::new("quit", None);
quit.connect_activate(glib::clone!(@weak application => move |_,_| {
application.quit();
}));
application.set_accels_for_action("app.quit", &["<Primary>Q"]);
let finder = gio::SimpleAction::new("finder", None);
finder.connect_activate(glib::clone!(@weak search_filter_button => move |_,_| {
search_filter_button.emit_clicked();
}));
application.set_accels_for_action("app.finder", &["<Primary>F"]);
application.add_action(&about);
application.add_action(&graphs);
application.add_action(&temperature);
application.add_action(&settings_action);
application.add_action(&new_task);
application.add_action(&quit);
application.add_action(&finder);
window.set_widget_name(utils::MAIN_WINDOW_NAME);
application.connect_activate(glib::clone!(@weak window => move |_| {
window.present();
}));
procs.search_bar.set_key_capture_widget(Some(&window));
fn revert_display(search_bar: >k::SearchBar) {
if search_bar.is_search_mode() {
search_bar.set_search_mode(false);
} else {
search_bar.set_search_mode(true);
}
}
search_filter_button.connect_clicked(glib::clone!(
@strong stack,
@weak procs.search_bar as procs_search_bar,
@weak network_tab,
=> move |_| {
if let Some(name) = stack.visible_child_name() {
match name.as_str() {
"Processes" => revert_display(&procs_search_bar),
"Networks" => revert_display(&network_tab.borrow().search_bar),
_ => {}
};
}
}));
stack.connect_visible_child_notify(move |s| {
if let Some(name) = s.visible_child_name() {
match name.as_str() {
"Processes" => {
procs.search_bar.set_key_capture_widget(Some(&window));
network_tab
.borrow()
.search_bar
.set_key_capture_widget(None::<>k::Widget>);
search_filter_button.set_sensitive(true);
return;
}
"Networks" => {
network_tab
.borrow()
.search_bar
.set_key_capture_widget(Some(&window));
procs
.search_bar
.set_key_capture_widget(None::<>k::Widget>);
search_filter_button.set_sensitive(true);
return;
}
_ => {}
}
}
search_filter_button.set_sensitive(false);
procs
.search_bar
.set_key_capture_widget(None::<>k::Widget>);
network_tab
.borrow()
.search_bar
.set_key_capture_widget(None::<>k::Widget>);
});
}
fn main() {
let application = gtk::Application::new(Some(APPLICATION_NAME), gio::ApplicationFlags::empty());
application.connect_startup(move |app| {
let provider = gtk::CssProvider::new();
provider.load_from_data(
br#"
graph_widget {
color: @theme_fg_color;
}
.titlebar-button {
background-color: @theme_bg_color;
border-radius: 4px;
}
.titlebar-button:hover {
background-color: shade(@theme_bg_color, 1.50);
}
.button-with-margin {
margin: 6px;
}
"#,
);
gtk::StyleContext::add_provider_for_display(
&gdk::Display::default().expect("Could not connect to a display."),
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
build_ui(app);
});
glib::set_application_name("process-viewer");
application.run();
}