diff --git a/Cargo.lock b/Cargo.lock index b0edbce..32bea88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -920,6 +920,18 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -978,6 +990,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "fragile" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" + [[package]] name = "freetype-sys" version = "0.20.1" @@ -997,6 +1015,7 @@ checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1078,10 +1097,13 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-macro", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", @@ -1161,8 +1183,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1621,6 +1645,8 @@ dependencies = [ "plotters", "plotters-cairo", "pretty_assertions", + "relm4", + "relm4-components", "tracing", "tracing-subscriber", ] @@ -1835,6 +1861,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "nix" version = "0.29.0" @@ -2294,6 +2329,52 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "relm4" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf0363f92b6a7eefd985b47f27b7ae168dd2fd5cd4013a338c9b111c33744d1f" +dependencies = [ + "flume", + "fragile", + "futures", + "gtk4", + "libadwaita", + "once_cell", + "relm4-css", + "relm4-macros", + "tokio", + "tracing", +] + +[[package]] +name = "relm4-components" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3d67f2982131c5e6047af4278d8fe750266767e57b58bc15f2e11e190eef36" +dependencies = [ + "once_cell", + "relm4", + "tracker", +] + +[[package]] +name = "relm4-css" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3b924557df1cddc687b60b313c4b76620fdbf0e463afa4b29f67193ccf37f9" + +[[package]] +name = "relm4-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5885640821d60062497737dd42fd04248d13c7ecccee620caaa4b210fe9905" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "rle-decode-fast" version = "1.0.3" @@ -2518,6 +2599,15 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2792,6 +2882,26 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracker" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5c98457ff700aaeefcd4a4a492096e78a2af1dd8523c66e94a3adb0fdbd415" +dependencies = [ + "tracker-macros", +] + +[[package]] +name = "tracker-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ttf-parser" version = "0.20.0" diff --git a/lact-client/src/lib.rs b/lact-client/src/lib.rs index 392b5c7..0767e96 100644 --- a/lact-client/src/lib.rs +++ b/lact-client/src/lib.rs @@ -100,7 +100,7 @@ impl DaemonClient { }) } - pub fn list_devices<'a>(&self) -> anyhow::Result>>> { + pub fn list_devices(&self) -> anyhow::Result>> { self.make_request(Request::ListDevices) } diff --git a/lact-daemon/src/server/handler.rs b/lact-daemon/src/server/handler.rs index 3a6c4b6..d5f0b60 100644 --- a/lact-daemon/src/server/handler.rs +++ b/lact-daemon/src/server/handler.rs @@ -277,15 +277,18 @@ impl<'a> Handler { .context("No controller with such id")?) } - pub fn list_devices(&'a self) -> Vec> { + pub fn list_devices(&'a self) -> Vec { self.gpu_controllers .iter() .map(|(id, controller)| { let name = controller .pci_info .as_ref() - .and_then(|pci_info| pci_info.device_pci_info.model.as_deref()); - DeviceListEntry { id, name } + .and_then(|pci_info| pci_info.device_pci_info.model.clone()); + DeviceListEntry { + id: id.to_owned(), + name, + } }) .collect() } diff --git a/lact-daemon/src/server/system.rs b/lact-daemon/src/server/system.rs index f8ecf50..d907570 100644 --- a/lact-daemon/src/server/system.rs +++ b/lact-daemon/src/server/system.rs @@ -14,13 +14,15 @@ const PP_OVERDRIVE_MASK: u64 = 0x4000; pub const PP_FEATURE_MASK_PATH: &str = "/sys/module/amdgpu/parameters/ppfeaturemask"; pub const MODULE_CONF_PATH: &str = "/etc/modprobe.d/99-amdgpu-overdrive.conf"; -pub async fn info() -> anyhow::Result> { - let version = env!("CARGO_PKG_VERSION"); +pub async fn info() -> anyhow::Result { + let version = env!("CARGO_PKG_VERSION").to_owned(); let profile = if cfg!(debug_assertions) { "debug" } else { "release" - }; + } + .to_owned(); + let kernel_output = Command::new("uname") .arg("-r") .output() @@ -42,7 +44,7 @@ pub async fn info() -> anyhow::Result> { profile, kernel_version, amdgpu_overdrive_enabled, - commit: Some(GIT_COMMIT), + commit: Some(GIT_COMMIT.to_owned()), }) } diff --git a/lact-gui/Cargo.toml b/lact-gui/Cargo.toml index 765e25d..b0588d7 100644 --- a/lact-gui/Cargo.toml +++ b/lact-gui/Cargo.toml @@ -8,10 +8,12 @@ edition = "2021" default = ["gtk-tests"] gtk-tests = [] bench = [] +adw = ["dep:adw", "relm4/libadwaita"] [dependencies] lact-client = { path = "../lact-client" } lact-daemon = { path = "../lact-daemon", default-features = false } +lact-schema = { path = "../lact-schema", features = ["args"] } amdgpu-sysfs = { workspace = true } tracing = { workspace = true } @@ -23,6 +25,8 @@ gtk = { version = "0.9", package = "gtk4", features = ["v4_6", "blueprint"] } adw = { package = "libadwaita", version = "0.7.0", features = [ "v1_4", ], optional = true } +relm4 = "0.9.0" +relm4-components = "0.9.0" plotters = { version = "0.3.5", default-features = false, features = [ "datetime", @@ -38,7 +42,6 @@ itertools = "0.13.0" criterion = "0.5.1" pretty_assertions = "1.4.0" lact-gui = { path = ".", features = ["bench"] } -lact-schema = { path = "../lact-schema", features = ["args"] } [[bench]] name = "gui" diff --git a/lact-gui/src/app/apply_revealer.rs b/lact-gui/src/app/apply_revealer.rs index dd53185..e00e94a 100644 --- a/lact-gui/src/app/apply_revealer.rs +++ b/lact-gui/src/app/apply_revealer.rs @@ -1,50 +1,62 @@ -use gtk::prelude::*; -use gtk::*; +use gtk::prelude::{BoxExt, ButtonExt, OrientableExt, WidgetExt}; +use relm4::{ComponentParts, ComponentSender, SimpleComponent}; -#[derive(Clone)] pub struct ApplyRevealer { - pub container: Revealer, - apply_button: Button, - reset_button: Button, + shown: bool, } -impl ApplyRevealer { - pub fn new() -> Self { - let container = Revealer::builder().transition_duration(150).build(); - let vbox = Box::new(Orientation::Horizontal, 5); +#[derive(Debug)] +pub enum ApplyRevealerMsg { + Show, + Hide, +} - let apply_button = Button::builder().label("Apply").hexpand(true).build(); - let reset_button = Button::builder().label("Reset").build(); +#[relm4::component(pub)] +impl SimpleComponent for ApplyRevealer { + type Init = (); - vbox.append(&apply_button); - vbox.append(&reset_button); + type Input = ApplyRevealerMsg; + type Output = super::AppMsg; - container.set_child(Some(&vbox)); + view! { + gtk::Revealer { + #[watch] + set_reveal_child: model.shown, - Self { - container, - apply_button, - reset_button, + gtk::Box { + set_orientation: gtk::Orientation::Horizontal, + set_spacing: 5, + + gtk::Button { + set_label: "Apply", + set_hexpand: true, + connect_clicked[sender] => move |_| { sender.output(super::AppMsg::ApplyChanges).unwrap(); }, + }, + + gtk::Button { + set_label: "Revert", + connect_clicked[sender] => move |_| { sender.output(super::AppMsg::RevertChanges).unwrap(); }, + }, + } } } - pub fn show(&self) { - self.container.set_reveal_child(true); + fn init( + _init: Self::Init, + root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = Self { shown: false }; + + let widgets = view_output!(); + + ComponentParts { widgets, model } } - pub fn hide(&self) { - self.container.set_reveal_child(false); - } - - pub fn connect_apply_button_clicked(&self, f: F) { - self.apply_button.connect_clicked(move |_| { - f(); - }); - } - - pub fn connect_reset_button_clicked(&self, f: F) { - self.reset_button.connect_clicked(move |_| { - f(); - }); + fn update(&mut self, msg: Self::Input, _sender: ComponentSender) { + match msg { + ApplyRevealerMsg::Show => self.shown = true, + ApplyRevealerMsg::Hide => self.shown = false, + } } } diff --git a/lact-gui/src/app/confirmation_dialog.rs b/lact-gui/src/app/confirmation_dialog.rs new file mode 100644 index 0000000..3099e28 --- /dev/null +++ b/lact-gui/src/app/confirmation_dialog.rs @@ -0,0 +1,54 @@ +use gtk::prelude::{DialogExt, GtkWindowExt, WidgetExt}; +use relm4::{ComponentParts, ComponentSender, SimpleComponent}; + +#[derive(Clone, Debug)] +pub struct ConfirmationOptions { + pub title: &'static str, + pub message: String, + pub buttons_type: gtk::ButtonsType, +} + +pub struct ConfirmationDialog {} + +#[relm4::component(pub)] +impl SimpleComponent for ConfirmationDialog { + type Init = (ConfirmationOptions, gtk::ApplicationWindow); + type Input = (); + type Output = gtk::ResponseType; + + view! { + gtk::MessageDialog { + set_transient_for: Some(&parent), + set_title: Some(options.title), + set_use_markup: true, + + connect_response[sender] => move |diag, response| { + sender.output(response).unwrap(); + diag.close(); + }, + } + } + + #[allow(unused_assignments)] + fn init( + (options, parent): Self::Init, + mut root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let model = Self {}; + + root = gtk::MessageDialog::new( + Some(&parent), + gtk::DialogFlags::MODAL, + gtk::MessageType::Question, + options.buttons_type, + options.message, + ); + + let widgets = view_output!(); + + root.show(); + + ComponentParts { model, widgets } + } +} diff --git a/lact-gui/src/app/header.rs b/lact-gui/src/app/header.rs index 43063be..48e7fb0 100644 --- a/lact-gui/src/app/header.rs +++ b/lact-gui/src/app/header.rs @@ -1,9 +1,96 @@ +use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow}; use gtk::prelude::*; use gtk::*; -use lact_client::schema::{DeviceListEntry, SystemInfo}; -use pango::EllipsizeMode; +use lact_client::schema::DeviceListEntry; +use relm4::{ + Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent, +}; +use relm4_components::simple_combo_box::SimpleComboBox; -#[derive(Clone)] +pub struct Header { + gpu_selector: Controller>, +} + +#[relm4::component(pub)] +impl SimpleComponent for Header { + type Init = (Vec, gtk::Stack); + type Input = (); + type Output = AppMsg; + + view! { + gtk::HeaderBar { + set_show_title_buttons: true, + + #[wrap(Some)] + set_title_widget = &StackSwitcher { + set_stack: Some(&stack), + }, + + #[local_ref] + pack_start = gpu_selector -> ComboBoxText, + + pack_end = >k::MenuButton { + set_icon_name: "open-menu-symbolic", + set_menu_model: Some(&app_menu), + } + } + } + + menu! { + app_menu: { + section! { + "Show historical charts" => ShowGraphsWindow, + }, + section! { + "Generate debug snapshot" => DebugSnapshot, + "Dump VBIOS" => DumpVBios, + } , + section! { + "Disable overclocking support" => DisableOverdrive, + "Reset all configuration" => ResetConfig, + } + } + } + + fn init( + (variants, stack): Self::Init, + root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + let gpu_selector = SimpleComboBox::builder() + .launch(SimpleComboBox { + variants, + active_index: Some(0), + }) + .forward(sender.output_sender(), |_| AppMsg::ReloadData { + full: true, + }); + + // limits the length of gpu names in combobox + for cell in gpu_selector.widget().cells() { + cell.set_property("width-chars", 10); + cell.set_property("ellipsize", pango::EllipsizeMode::End); + } + + let model = Self { gpu_selector }; + + let gpu_selector = model.gpu_selector.widget(); + let widgets = view_output!(); + + ComponentParts { model, widgets } + } +} + +impl Header { + pub fn selected_gpu_id(&self) -> Option { + self.gpu_selector + .model() + .get_active_elem() + .map(|model| model.id.clone()) + } +} + +/*#[derive(Clone)] pub struct Header { pub container: HeaderBar, gpu_selector: ComboBoxText, @@ -80,4 +167,4 @@ impl Header { } }); } -} +}*/ diff --git a/lact-gui/src/app/info_row.rs b/lact-gui/src/app/info_row.rs index e953930..d9cfbc8 100644 --- a/lact-gui/src/app/info_row.rs +++ b/lact-gui/src/app/info_row.rs @@ -13,6 +13,14 @@ impl InfoRow { .property("value", value) .build() } + + pub fn new_selectable(name: &str, value: &str) -> Self { + Object::builder() + .property("name", name) + .property("value", value) + .property("selectable", true) + .build() + } } mod imp { diff --git a/lact-gui/src/app/mod.rs b/lact-gui/src/app/mod.rs index c4410c8..dd231f4 100644 --- a/lact-gui/src/app/mod.rs +++ b/lact-gui/src/app/mod.rs @@ -1,378 +1,302 @@ mod apply_revealer; +mod confirmation_dialog; mod graphs_window; mod header; mod info_row; +mod msg; mod page_section; mod root_stack; #[cfg(feature = "bench")] pub use graphs_window::plot::{Plot, PlotData}; -use self::graphs_window::GraphsWindow; use crate::{create_connection, APP_ID, GUI_VERSION}; use anyhow::{anyhow, Context}; -use apply_revealer::ApplyRevealer; -use glib::clone; -use gtk::gio::ActionEntry; -use gtk::glib::{timeout_future, ControlFlow}; -use gtk::{gio::ApplicationFlags, prelude::*, *}; +use apply_revealer::{ApplyRevealer, ApplyRevealerMsg}; +use confirmation_dialog::ConfirmationDialog; +use graphs_window::GraphsWindow; +use gtk::{ + glib::{self, clone, ControlFlow}, + prelude::{ + BoxExt, ButtonExt, Cast, DialogExtManual, FileChooserExt, FileExt, GtkWindowExt, + OrientableExt, WidgetExt, + }, + ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog, + MessageType, ResponseType, +}; use header::Header; -use lact_client::schema::request::{ConfirmCommand, SetClocksCommand}; -use lact_client::schema::{FanOptions, GIT_COMMIT}; use lact_client::DaemonClient; use lact_daemon::MODULE_CONF_PATH; +use lact_schema::{ + request::{ConfirmCommand, SetClocksCommand}, + FanOptions, GIT_COMMIT, +}; +use msg::AppMsg; +use relm4::{ + actions::{RelmAction, RelmActionGroup}, + tokio, Component, ComponentController, ComponentParts, ComponentSender, +}; use root_stack::RootStack; -use std::cell::RefCell; -use std::rc::Rc; -use std::sync::atomic::AtomicBool; -use std::time::Duration; +use std::{cell::RefCell, rc::Rc, sync::atomic::AtomicBool, time::Duration}; use tracing::{debug, error, trace, warn}; -// In ms -const STATS_POLL_INTERVAL: u64 = 250; +const STATS_POLL_INTERVAL_MS: u64 = 250; -#[derive(Clone)] -pub(crate) struct App { - application: Application, - pub window: ApplicationWindow, - pub header: Header, - root_stack: RootStack, - apply_revealer: ApplyRevealer, +pub struct AppModel { daemon_client: DaemonClient, graphs_window: GraphsWindow, + root_stack: RootStack, + header: relm4::Controller
, + apply_revealer: relm4::Controller, + stats_task_handle: Option>, } -impl App { - pub fn new(daemon_client: DaemonClient) -> Self { - #[cfg(feature = "adw")] - let application: Application = - adw::Application::new(Some(APP_ID), ApplicationFlags::default()).upcast(); - #[cfg(not(feature = "adw"))] - let application = Application::new(Some(APP_ID), ApplicationFlags::default()); +#[relm4::component(pub)] +impl Component for AppModel { + type Init = (DaemonClient, Option); + + type Input = AppMsg; + type Output = (); + type CommandOutput = (); + + view! { + #[name = "root_window"] + gtk::ApplicationWindow { + set_title: Some("LACT"), + set_default_width: 600, + set_default_height: 900, + set_icon_name: Some(APP_ID), + set_titlebar: Some(model.header.widget()), + + #[name = "root_box"] + gtk::Box { + set_orientation: gtk::Orientation::Vertical, + set_spacing: 5, + + model.root_stack.container.clone(), + model.apply_revealer.widget(), + } + } + } + + fn init( + (daemon_client, conn_err): Self::Init, + root: Self::Root, + sender: ComponentSender, + ) -> ComponentParts { + register_actions(&sender); let system_info_buf = daemon_client .get_system_info() .expect("Could not fetch system info"); let system_info = system_info_buf.inner().expect("Invalid system info buffer"); - let header = Header::new(&system_info); - let window = ApplicationWindow::builder() - .title("LACT") - .default_width(600) - .default_height(860) - .icon_name(APP_ID) - .build(); + let devices_buf = daemon_client + .list_devices() + .expect("Could not list devices"); + let devices = devices_buf.inner().expect("Could not access devices"); - if system_info.version != GUI_VERSION || system_info.commit != Some(GIT_COMMIT) { - let err = anyhow!("Version mismatch between GUI and daemon ({GUI_VERSION}-{GIT_COMMIT} vs {}-{})! Make sure you have restarted the service if you have updated LACT.", system_info.version, system_info.commit.unwrap_or_default()); - show_error(&window, err); + if system_info.version != GUI_VERSION || system_info.commit.as_deref() != Some(GIT_COMMIT) { + let err = anyhow!("Version mismatch between GUI and daemon ({GUI_VERSION}-{GIT_COMMIT} vs {}-{})! Make sure you have restarted the service if you have updated LACT.", system_info.version, system_info.commit.as_deref().unwrap_or_default()); + sender.input(AppMsg::Error(err.into())); } - window.set_titlebar(Some(&header.container)); - let root_stack = RootStack::new(system_info, daemon_client.embedded); - header.set_switcher_stack(&root_stack.container); + let header = Header::builder() + .launch((devices, root_stack.container.clone())) + .forward(sender.input_sender(), |msg| msg); - let root_box = Box::new(Orientation::Vertical, 5); + let apply_revealer = ApplyRevealer::builder() + .launch(()) + .forward(sender.input_sender(), |msg| msg); - root_box.append(&root_stack.container); - - let apply_revealer = ApplyRevealer::new(); - - root_box.append(&apply_revealer.container); - - window.set_child(Some(&root_box)); - - let graphs_window = GraphsWindow::new(); - - App { - application, - window, - header, - root_stack, - apply_revealer, - daemon_client, - graphs_window, - } - } - - pub fn run(self, connection_err: Option) -> anyhow::Result<()> { - self.application.connect_activate(clone!( - #[strong(rename_to = app)] - self, - move |_| { - app.window.set_application(Some(&app.application)); - - let current_gpu_id = Rc::new(RefCell::new(String::new())); - - app.header.connect_gpu_selection_changed(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move |gpu_id| { - debug!("GPU Selection changed"); - app.set_info(&gpu_id); - *current_gpu_id.borrow_mut() = gpu_id; - debug!("Updated current GPU id"); - app.graphs_window.clear(); - } - )); - - let devices_buf = app - .daemon_client - .list_devices() - .expect("Could not list devices"); - let devices = devices_buf.inner().expect("Could not access devices"); - app.header.set_devices(&devices); - - app.root_stack - .oc_page - .clocks_frame - .connect_clocks_reset(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move || { - debug!("Resetting clocks"); - - let gpu_id = current_gpu_id.borrow().clone(); - - match app - .daemon_client - .set_clocks_value(&gpu_id, SetClocksCommand::Reset) - .and_then(|_| { - app.daemon_client - .confirm_pending_config(ConfirmCommand::Confirm) - }) { - Ok(()) => { - app.set_initial(&gpu_id); - } - Err(err) => { - show_error(&app.window, err); - } - } - } - )); - - app.root_stack.thermals_page.connect_reset_pmfw(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move || { - debug!("Resetting PMFW settings"); - let gpu_id = current_gpu_id.borrow().clone(); - - match app - .daemon_client - .reset_pmfw(&gpu_id) - .and_then(|buffer| buffer.inner()) - .and_then(|_| { - app.daemon_client - .confirm_pending_config(ConfirmCommand::Confirm) - }) { - Ok(()) => { - app.set_initial(&gpu_id); - } - Err(err) => { - show_error(&app.window, err); - } - } - } - )); - - app.apply_revealer.connect_apply_button_clicked(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move || { - glib::idle_add_local_once(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move || { - if let Err(err) = app - .apply_settings(current_gpu_id.clone()) - .context("Could not apply settings (GUI)") - { - show_error(&app.window, err); - - glib::idle_add_local_once(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move || { - let gpu_id = current_gpu_id.borrow().clone(); - app.set_initial(&gpu_id) - } - )); - } - } - )); - } - )); - app.apply_revealer.connect_reset_button_clicked(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move || { - let gpu_id = current_gpu_id.borrow().clone(); - app.set_initial(&gpu_id) - } - )); - - if let Some(ref button) = app.root_stack.oc_page.enable_overclocking_button { - button.connect_clicked(clone!( - #[strong] - app, - move |_| { - app.enable_overclocking(); - } - )); - } - - let snapshot_action = ActionEntry::builder("generate-debug-snapshot") - .activate(clone!( - #[strong] - app, - move |_, _, _| { - app.generate_debug_snapshot(); - } - )) - .build(); - - let disable_overdive_action = ActionEntry::builder("disable-overdrive") - .activate(clone!( - #[strong] - app, - move |_, _, _| { - app.disable_overclocking(); - } - )) - .build(); - - let show_graphs_window_action = ActionEntry::builder("show-graphs-window") - .activate(clone!( - #[strong] - app, - move |_, _, _| { - app.graphs_window.show(); - } - )) - .build(); - - let dump_vbios_action = ActionEntry::builder("dump-vbios") - .activate(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move |_, _, _| { - let gpu_id = current_gpu_id.borrow(); - app.dump_vbios(&gpu_id); - } - )) - .build(); - - let reset_config_action = ActionEntry::builder("reset-config") - .activate(clone!( - #[strong] - app, - #[strong] - current_gpu_id, - move |_, _, _| { - let gpu_id = current_gpu_id.borrow().clone(); - app.reset_config(gpu_id); - } - )) - .build(); - - app.application.add_action_entries([ - snapshot_action, - disable_overdive_action, - show_graphs_window_action, - dump_vbios_action, - reset_config_action, - ]); - - app.start_stats_update_loop(current_gpu_id); - - app.window.show(); - - if app.daemon_client.embedded { - let error_text = connection_err - .as_ref() - .map(|err| format!("Error info: {err:#}\n\n")) - .unwrap_or_default(); - - let text = format!( - "Could not connect to daemon, running in embedded mode. \n\ - Please make sure the lactd service is running. \n\ - Using embedded mode, you will not be able to change any settings. \n\n\ - {error_text}\ - To enable the daemon, run the following command:" - ); - - let text_label = Label::new(Some(&text)); - let enable_label = Entry::builder() - .text("sudo systemctl enable --now lactd") - .editable(false) - .build(); - - let vbox = Box::builder() - .orientation(Orientation::Vertical) - .margin_top(10) - .margin_bottom(10) - .margin_start(10) - .margin_end(10) - .build(); - - let close_button = Button::builder().label("Close").build(); - - vbox.append(&text_label); - vbox.append(&enable_label); - vbox.append(&close_button); - - let diag = MessageDialog::builder() - .title("Daemon info") - .message_type(MessageType::Warning) - .child(&vbox) - .transient_for(&app.window) - .build(); - - close_button.connect_clicked(clone!( - #[strong] - diag, - move |_| diag.hide() - )); - - diag.run_async(|diag, _| { - diag.hide(); - }) - } + root_stack.oc_page.clocks_frame.connect_clocks_reset(clone!( + #[strong] + sender, + move || { + sender.input(AppMsg::ResetClocks); + } + )); + root_stack.thermals_page.connect_reset_pmfw(clone!( + #[strong] + sender, + move || { + sender.input(AppMsg::ResetPmfw); } )); - // Args are passed manually since they were already processed by clap before - self.application.run_with_args::(&[]); - Ok(()) + if let Some(ref button) = root_stack.oc_page.enable_overclocking_button { + button.connect_clicked(clone!( + #[strong] + sender, + move |_| { + sender.input(AppMsg::ask_confirmation( + AppMsg::EnableOverdrive, + "Enable Overclocking", + format!("This will enable the overdrive feature of the amdgpu driver by creating a file at {MODULE_CONF_PATH} and updating the initramfs. Are you sure you want to do this?"), + gtk::ButtonsType::OkCancel, + )); + } + )); + } + + let graphs_window = GraphsWindow::new(); + + let model = AppModel { + daemon_client, + graphs_window, + root_stack, + apply_revealer, + header, + stats_task_handle: None, + }; + + let widgets = view_output!(); + + let embedded = model.daemon_client.embedded; + let conn_err = RefCell::new(conn_err); + root.connect_visible_notify(move |root| { + if embedded { + if let Some(err) = conn_err.borrow_mut().take() { + show_embedded_info(root, err); + } + } + }); + + sender.input(AppMsg::ReloadData { full: true }); + + ComponentParts { model, widgets } } - fn set_info(&self, gpu_id: &str) { - let info_buf = self - .daemon_client - .get_device_info(gpu_id) - .expect("Could not fetch info"); - let info = info_buf.inner().unwrap(); + fn update_with_view( + &mut self, + widgets: &mut Self::Widgets, + msg: Self::Input, + sender: ComponentSender, + root: &Self::Root, + ) { + trace!("update {msg:#?}"); + if let Err(err) = self.handle_msg(msg, sender.clone(), root) { + show_error(root, &err); + } + self.update_view(widgets, sender); + } +} - trace!("setting info {info:?}"); +impl AppModel { + fn handle_msg( + &mut self, + msg: AppMsg, + sender: ComponentSender, + root: >k::ApplicationWindow, + ) -> Result<(), Rc> { + match msg { + AppMsg::Error(err) => Err(err), + AppMsg::ReloadData { full } => { + let gpu_id = self.current_gpu_id()?; + if full { + self.update_gpu_data_full(gpu_id, sender)?; + } else { + self.update_gpu_data(gpu_id, sender)?; + } + Ok(()) + } + AppMsg::Stats(stats) => { + self.root_stack.info_page.set_stats(&stats); + self.root_stack.thermals_page.set_stats(&stats, false); + self.root_stack.oc_page.set_stats(&stats, false); + self.graphs_window.set_stats(&stats); + Ok(()) + } + AppMsg::ApplyChanges => self + .apply_settings(self.current_gpu_id()?, root, &sender) + .map_err(|err| { + sender.input(AppMsg::ReloadData { full: false }); + err.into() + }), + AppMsg::RevertChanges => { + sender.input(AppMsg::ReloadData { full: false }); + Ok(()) + } + AppMsg::ResetClocks => { + let gpu_id = self.current_gpu_id()?; + self.daemon_client + .set_clocks_value(&gpu_id, SetClocksCommand::Reset)?; + self.daemon_client + .confirm_pending_config(ConfirmCommand::Confirm)?; + sender.input(AppMsg::ReloadData { full: false }); + + Ok(()) + } + AppMsg::ResetPmfw => { + let gpu_id = self.current_gpu_id()?; + self.daemon_client.reset_pmfw(&gpu_id)?; + self.daemon_client + .confirm_pending_config(ConfirmCommand::Confirm)?; + sender.input(AppMsg::ReloadData { full: false }); + + Ok(()) + } + AppMsg::ShowGraphsWindow => { + self.graphs_window.show(); + Ok(()) + } + AppMsg::DumpVBios => { + self.dump_vbios(&self.current_gpu_id()?, root); + Ok(()) + } + AppMsg::DebugSnapshot => { + self.generate_debug_snapshot(root); + Ok(()) + } + AppMsg::EnableOverdrive => { + toggle_overdrive(true, root.clone()); + Ok(()) + } + AppMsg::DisableOverdrive => { + toggle_overdrive(false, root.clone()); + Ok(()) + } + AppMsg::ResetConfig => { + self.daemon_client.reset_config()?; + sender.input(AppMsg::ReloadData { full: true }); + Ok(()) + } + AppMsg::AskConfirmation(options, confirmed_msg) => { + let sender = sender.clone(); + + let mut controller = ConfirmationDialog::builder() + .launch((options, root.clone())) + .connect_receiver(move |_, response| { + if let gtk::ResponseType::Ok | gtk::ResponseType::Yes = response { + sender.input(*confirmed_msg.clone()); + } + }); + controller.detach_runtime(); + + Ok(()) + } + } + } + + fn current_gpu_id(&self) -> anyhow::Result { + self.header + .model() + .selected_gpu_id() + .context("No GPU selected") + } + + fn update_gpu_data_full( + &mut self, + gpu_id: String, + sender: ComponentSender, + ) -> anyhow::Result<()> { + let daemon_client = self.daemon_client.clone(); + let info_buf = daemon_client + .get_device_info(&gpu_id) + .context("Could not fetch info")?; + let info = info_buf.inner()?; self.root_stack.info_page.set_info(&info); self.root_stack.oc_page.set_info(&info); @@ -384,23 +308,35 @@ impl App { .unwrap_or(1.0); self.graphs_window.set_vram_clock_ratio(vram_clock_ratio); - self.set_initial(gpu_id); + self.update_gpu_data(gpu_id, sender)?; + self.root_stack.thermals_page.set_info(&info); + + Ok(()) } - fn set_initial(&self, gpu_id: &str) { - debug!("setting initial stats for gpu {gpu_id}"); - let stats_buf = self + fn update_gpu_data( + &mut self, + gpu_id: String, + sender: ComponentSender, + ) -> anyhow::Result<()> { + if let Some(stats_task) = self.stats_task_handle.take() { + stats_task.abort(); + } + + debug!("updating info for gpu {gpu_id}"); + + let stats = self .daemon_client - .get_device_stats(gpu_id) - .expect("Could not fetch stats"); - let stats = stats_buf.inner().unwrap(); + .get_device_stats(&gpu_id) + .context("Could not fetch stats")? + .inner()?; self.root_stack.oc_page.set_stats(&stats, true); self.root_stack.thermals_page.set_stats(&stats, true); self.root_stack.info_page.set_stats(&stats); - let maybe_clocks_table = match self.daemon_client.get_device_clocks_info(gpu_id) { + let maybe_clocks_table = match self.daemon_client.get_device_clocks_info(&gpu_id) { Ok(clocks_buf) => match clocks_buf.inner() { Ok(info) => info.table, Err(err) => { @@ -415,7 +351,7 @@ impl App { }; self.root_stack.oc_page.set_clocks_table(maybe_clocks_table); - let maybe_modes_table = match self.daemon_client.get_device_power_profile_modes(gpu_id) { + let maybe_modes_table = match self.daemon_client.get_device_power_profile_modes(&gpu_id) { Ok(buf) => match buf.inner() { Ok(table) => Some(table), Err(err) => { @@ -435,7 +371,7 @@ impl App { match self .daemon_client - .get_power_states(gpu_id) + .get_power_states(&gpu_id) .and_then(|states| states.inner()) { Ok(power_states) => { @@ -450,11 +386,10 @@ impl App { // Show apply button on setting changes // This is done here because new widgets may appear after applying settings (like fan curve points) which should be connected let show_revealer = clone!( - #[strong(rename_to = apply_revealer)] - self.apply_revealer, + #[strong(rename_to = apply_sender)] + self.apply_revealer.sender(), move || { - debug!("settings changed, showing apply button"); - apply_revealer.show(); + apply_sender.send(ApplyRevealerMsg::Show).unwrap(); } ); @@ -466,51 +401,31 @@ impl App { .oc_page .connect_settings_changed(show_revealer); - self.apply_revealer.hide(); - } + self.apply_revealer + .sender() + .send(ApplyRevealerMsg::Hide) + .unwrap(); - fn start_stats_update_loop(&self, current_gpu_id: Rc>) { - // The loop that gets stats - glib::spawn_future_local(clone!( - #[strong(rename_to = daemon_client)] - self.daemon_client, - #[strong(rename_to = root_stack)] - self.root_stack, - #[strong(rename_to = graphs_window)] - self.graphs_window, - async move { - loop { - { - let gpu_id = current_gpu_id.borrow(); - trace!("fetching new stats using id {gpu_id}"); - match daemon_client - .get_device_stats(&gpu_id) - .and_then(|stats| stats.inner()) - { - Ok(stats) => { - trace!("new stats received, updating {stats:?}"); - root_stack.info_page.set_stats(&stats); - root_stack.thermals_page.set_stats(&stats, false); - root_stack.oc_page.set_stats(&stats, false); - graphs_window.set_stats(&stats); - } - Err(err) => { - error!("Could not fetch stats: {err}"); - } - } - } - timeout_future(Duration::from_millis(STATS_POLL_INTERVAL)).await; - } - } + self.graphs_window.clear(); + + self.stats_task_handle = Some(start_stats_update_loop( + gpu_id.to_owned(), + self.daemon_client.clone(), + sender, )); + + Ok(()) } - fn apply_settings(&self, current_gpu_id: Rc>) -> anyhow::Result<()> { + fn apply_settings( + &self, + gpu_id: String, + root: >k::ApplicationWindow, + sender: &ComponentSender, + ) -> anyhow::Result<()> { // TODO: Ask confirmation for everything, not just clocks - debug!("applying settings"); - let gpu_id = current_gpu_id.borrow().clone(); - debug!("using gpu {gpu_id}"); + debug!("applying settings on gpu {gpu_id}"); if let Some(cap) = self.root_stack.oc_page.get_power_cap() { self.daemon_client @@ -630,237 +545,27 @@ impl App { .daemon_client .batch_set_clocks_value(&gpu_id, clocks_commands) .context("Could not commit clocks settings")?; - self.ask_settings_confirmation(gpu_id.clone(), delay); + self.ask_settings_confirmation(delay, root, sender); } - self.set_initial(&gpu_id); + sender.input(AppMsg::ReloadData { full: false }); Ok(()) } - fn generate_debug_snapshot(&self) { - match self - .daemon_client - .generate_debug_snapshot() - .and_then(|response| response.inner()) - { - Ok(path) => { - let path_label = Label::builder() - .use_markup(true) - .label(format!("{path}")) - .selectable(true) - .build(); - - let vbox = Box::builder() - .orientation(Orientation::Vertical) - .margin_top(10) - .margin_bottom(10) - .margin_start(10) - .margin_end(10) - .build(); - - vbox.append(&Label::new(Some("Debug snapshot saved at:"))); - vbox.append(&path_label); - - let diag = MessageDialog::builder() - .title("Snapshot generated") - .message_type(MessageType::Info) - .use_markup(true) - .text(format!("Debug snapshot saved at {path}")) - .buttons(ButtonsType::Ok) - .transient_for(&self.window) - .build(); - - let message_box = diag.message_area().downcast::().unwrap(); - for child in message_box.observe_children().into_iter().flatten() { - if let Ok(label) = child.downcast::