mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2025-02-25 18:55:26 -06:00
feat: add support for multiple settings profiles (#327)
* feat: initial support for multiple settings profiles * feat: initial profiles management support * feat: basic profile management ui
This commit is contained in:
parent
fa0eb3083c
commit
538cea3aa5
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -1468,9 +1468,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.4.0"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@ -1611,6 +1611,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"futures",
|
||||
"indexmap",
|
||||
"lact-schema",
|
||||
"libdrm_amdgpu_sys",
|
||||
"libflate",
|
||||
|
@ -23,6 +23,7 @@ futures = { version = "0.3.30", default-features = false }
|
||||
tokio = { version = "1.35.1", default-features = false }
|
||||
nix = { version = "0.29.0", default-features = false }
|
||||
chrono = "0.4.31"
|
||||
indexmap = { version = "2.5.0", features = ["serde"] }
|
||||
|
||||
[profile.release]
|
||||
strip = "symbols"
|
||||
|
@ -3,6 +3,7 @@ mod connection;
|
||||
mod macros;
|
||||
|
||||
pub use lact_schema as schema;
|
||||
use lact_schema::request::ProfileBase;
|
||||
|
||||
use amdgpu_sysfs::gpu_handle::{
|
||||
power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind,
|
||||
@ -12,8 +13,8 @@ use connection::{tcp::TcpConnection, unix::UnixConnection, DaemonConnection};
|
||||
use nix::unistd::getuid;
|
||||
use schema::{
|
||||
request::{ConfirmCommand, SetClocksCommand},
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, Request,
|
||||
Response, SystemInfo,
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, ProfilesInfo,
|
||||
Request, Response, SystemInfo,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
@ -116,6 +117,7 @@ impl DaemonClient {
|
||||
request_plain!(disable_overdrive, DisableOverdrive, String);
|
||||
request_plain!(generate_debug_snapshot, GenerateSnapshot, String);
|
||||
request_plain!(reset_config, RestConfig, ());
|
||||
request_plain!(list_profiles, ListProfiles, ProfilesInfo);
|
||||
request_with_id!(get_device_info, DeviceInfo, DeviceInfo);
|
||||
request_with_id!(get_device_stats, DeviceStats, DeviceStats);
|
||||
request_with_id!(get_device_clocks_info, DeviceClocksInfo, ClocksInfo);
|
||||
@ -128,6 +130,24 @@ impl DaemonClient {
|
||||
request_with_id!(reset_pmfw, ResetPmfw, u64);
|
||||
request_with_id!(dump_vbios, VbiosDump, Vec<u8>);
|
||||
|
||||
pub async fn set_profile(&self, name: Option<String>) -> anyhow::Result<()> {
|
||||
self.make_request(Request::SetProfile { name })
|
||||
.await?
|
||||
.inner()
|
||||
}
|
||||
|
||||
pub async fn create_profile(&self, name: String, base: ProfileBase) -> anyhow::Result<()> {
|
||||
self.make_request(Request::CreateProfile { name, base })
|
||||
.await?
|
||||
.inner()
|
||||
}
|
||||
|
||||
pub async fn delete_profile(&self, name: String) -> anyhow::Result<()> {
|
||||
self.make_request(Request::DeleteProfile { name })
|
||||
.await?
|
||||
.inner()
|
||||
}
|
||||
|
||||
pub async fn set_performance_level(
|
||||
&self,
|
||||
id: &str,
|
||||
|
@ -27,6 +27,7 @@ tokio = { workspace = true, features = [
|
||||
"sync",
|
||||
] }
|
||||
futures = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
|
||||
pciid-parser = { version = "0.7", features = ["serde"] }
|
||||
serde_yaml = "0.9"
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::server::gpu_controller::fan_control::FanCurve;
|
||||
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
||||
use anyhow::Context;
|
||||
use indexmap::IndexMap;
|
||||
use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions};
|
||||
use nix::unistd::getuid;
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
@ -27,7 +28,11 @@ pub struct Config {
|
||||
#[serde(default = "default_apply_settings_timer")]
|
||||
pub apply_settings_timer: u64,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub gpus: HashMap<String, Gpu>,
|
||||
gpus: HashMap<String, Gpu>,
|
||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub profiles: IndexMap<String, Profile>,
|
||||
#[serde(default)]
|
||||
pub current_profile: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
@ -36,6 +41,8 @@ impl Default for Config {
|
||||
daemon: Daemon::default(),
|
||||
apply_settings_timer: default_apply_settings_timer(),
|
||||
gpus: HashMap::new(),
|
||||
profiles: IndexMap::new(),
|
||||
current_profile: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,6 +68,12 @@ impl Default for Daemon {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct Profile {
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub gpus: HashMap<String, Gpu>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct Gpu {
|
||||
@ -178,6 +191,54 @@ impl Config {
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the GPU configs according to the current profile. Returns an error if the current profile could not be found.
|
||||
pub fn gpus(&self) -> anyhow::Result<&HashMap<String, Gpu>> {
|
||||
match &self.current_profile {
|
||||
Some(profile) => {
|
||||
let profile = self
|
||||
.profiles
|
||||
.get(profile)
|
||||
.with_context(|| format!("Could not find profile '{profile}'"))?;
|
||||
Ok(&profile.gpus)
|
||||
}
|
||||
None => Ok(&self.gpus),
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as [`gpus`], but with a mutable reference
|
||||
pub fn gpus_mut(&mut self) -> anyhow::Result<&mut HashMap<String, Gpu>> {
|
||||
match &self.current_profile {
|
||||
Some(profile) => {
|
||||
let profile = self
|
||||
.profiles
|
||||
.get_mut(profile)
|
||||
.with_context(|| format!("Could not find profile '{profile}'"))?;
|
||||
Ok(&mut profile.gpus)
|
||||
}
|
||||
None => Ok(&mut self.gpus),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a specific profile
|
||||
pub fn profile(&self, profile: &str) -> anyhow::Result<&Profile> {
|
||||
self.profiles
|
||||
.get(profile)
|
||||
.with_context(|| format!("Profile {profile} not found"))
|
||||
}
|
||||
|
||||
/// Get the settings for "default" profile (aka no profile)
|
||||
pub fn default_profile(&self) -> Profile {
|
||||
Profile {
|
||||
gpus: self.gpus.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.gpus.clear();
|
||||
self.profiles.clear();
|
||||
self.current_profile = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_watcher(config_last_saved: Arc<Mutex<Instant>>) -> mpsc::UnboundedReceiver<Config> {
|
||||
|
@ -18,7 +18,7 @@ use tokio::{
|
||||
signal::unix::{signal, SignalKind},
|
||||
task::LocalSet,
|
||||
};
|
||||
use tracing::{debug, debug_span, info, warn, Instrument, Level};
|
||||
use tracing::{debug, debug_span, error, info, warn, Instrument, Level};
|
||||
|
||||
/// RDNA3, minimum family that supports the new pmfw interface
|
||||
pub const AMDGPU_FAMILY_GC_11_0_0: u32 = 145;
|
||||
@ -109,9 +109,15 @@ async fn listen_config_changes(handler: Handler) {
|
||||
while let Some(new_config) = rx.recv().await {
|
||||
info!("config file was changed, reloading");
|
||||
handler.config.replace(new_config);
|
||||
handler.apply_current_config().await;
|
||||
match handler.apply_current_config().await {
|
||||
Ok(()) => {
|
||||
info!("configuration reloaded");
|
||||
}
|
||||
Err(err) => {
|
||||
error!("could not apply new config: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_sufficient_uptime() {
|
||||
|
@ -164,6 +164,10 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho
|
||||
ok_response(handler.set_enabled_power_states(id, kind, states).await?)
|
||||
}
|
||||
Request::VbiosDump { id } => ok_response(handler.vbios_dump(id)?),
|
||||
Request::ListProfiles => ok_response(handler.list_profiles()),
|
||||
Request::SetProfile { name } => ok_response(handler.set_profile(name).await?),
|
||||
Request::CreateProfile { name, base } => ok_response(handler.create_profile(name, base)?),
|
||||
Request::DeleteProfile { name } => ok_response(handler.delete_profile(name).await?),
|
||||
Request::EnableOverdrive => ok_response(system::enable_overdrive().await?),
|
||||
Request::DisableOverdrive => ok_response(system::disable_overdrive().await?),
|
||||
Request::GenerateSnapshot => ok_response(handler.generate_snapshot().await?),
|
@ -2,17 +2,17 @@ use super::{
|
||||
gpu_controller::{fan_control::FanCurve, GpuController},
|
||||
system::{self, detect_initramfs_type, PP_FEATURE_MASK_PATH},
|
||||
};
|
||||
use crate::config::{self, default_fan_static_speed, Config, FanControlSettings};
|
||||
use crate::config::{self, default_fan_static_speed, Config, FanControlSettings, Profile};
|
||||
use amdgpu_sysfs::{
|
||||
gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind},
|
||||
sysfs::SysFS,
|
||||
};
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use lact_schema::{
|
||||
default_fan_curve,
|
||||
request::{ConfirmCommand, SetClocksCommand},
|
||||
request::{ConfirmCommand, ProfileBase, SetClocksCommand},
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions,
|
||||
PowerStates,
|
||||
PowerStates, ProfilesInfo,
|
||||
};
|
||||
use libflate::gzip;
|
||||
use nix::libc;
|
||||
@ -135,7 +135,9 @@ impl<'a> Handler {
|
||||
confirm_config_tx: Rc::new(RefCell::new(None)),
|
||||
config_last_saved: Arc::new(Mutex::new(Instant::now())),
|
||||
};
|
||||
handler.apply_current_config().await;
|
||||
if let Err(err) = handler.apply_current_config().await {
|
||||
error!("could not apply config: {err:#}");
|
||||
}
|
||||
|
||||
// Eagerly release memory
|
||||
// `load_controllers` allocates and deallocates the entire PCI ID database,
|
||||
@ -148,10 +150,11 @@ impl<'a> Handler {
|
||||
Ok(handler)
|
||||
}
|
||||
|
||||
pub async fn apply_current_config(&self) {
|
||||
pub async fn apply_current_config(&self) -> anyhow::Result<()> {
|
||||
let config = self.config.borrow().clone(); // Clone to avoid locking the RwLock on an await point
|
||||
|
||||
for (id, gpu_config) in &config.gpus {
|
||||
let gpus = config.gpus()?;
|
||||
for (id, gpu_config) in gpus {
|
||||
if let Some(controller) = self.gpu_controllers.get(id) {
|
||||
if let Err(err) = controller.apply_config(gpu_config).await {
|
||||
error!("could not apply existing config for gpu {id}: {err}");
|
||||
@ -160,6 +163,8 @@ impl<'a> Handler {
|
||||
info!("could not find GPU with id {id} defined in configuration");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn edit_gpu_config<F: FnOnce(&mut config::Gpu)>(
|
||||
@ -181,7 +186,7 @@ impl<'a> Handler {
|
||||
let (gpu_config, apply_timer) = {
|
||||
let config = self.config.try_borrow().map_err(|err| anyhow!("{err}"))?;
|
||||
let apply_timer = config.apply_settings_timer;
|
||||
let gpu_config = config.gpus.get(&id).cloned().unwrap_or_default();
|
||||
let gpu_config = config.gpus()?.get(&id).cloned().unwrap_or_default();
|
||||
(gpu_config, apply_timer)
|
||||
};
|
||||
|
||||
@ -243,7 +248,12 @@ impl<'a> Handler {
|
||||
*handler.config_last_saved.lock().unwrap() = Instant::now();
|
||||
|
||||
let mut config_guard = handler.config.borrow_mut();
|
||||
config_guard.gpus.insert(id, new_config);
|
||||
match config_guard.gpus_mut() {
|
||||
Ok(gpus) => {
|
||||
gpus.insert(id, new_config);
|
||||
}
|
||||
Err(err) => error!("{err:#}"),
|
||||
}
|
||||
|
||||
if let Err(err) = config_guard.save() {
|
||||
error!("{err:#}");
|
||||
@ -302,7 +312,7 @@ impl<'a> Handler {
|
||||
.config
|
||||
.try_borrow()
|
||||
.map_err(|err| anyhow!("Could not read config: {err:?}"))?;
|
||||
let gpu_config = config.gpus.get(id);
|
||||
let gpu_config = config.gpus()?.get(id);
|
||||
Ok(self.controller_by_id(id)?.get_stats(gpu_config))
|
||||
}
|
||||
|
||||
@ -316,7 +326,10 @@ impl<'a> Handler {
|
||||
.config
|
||||
.try_borrow_mut()
|
||||
.map_err(|err| anyhow!("{err}"))?;
|
||||
let gpu_config = config_guard.gpus.entry(opts.id.to_owned()).or_default();
|
||||
let gpu_config = config_guard
|
||||
.gpus_mut()?
|
||||
.entry(opts.id.to_owned())
|
||||
.or_default();
|
||||
|
||||
match opts.mode {
|
||||
Some(mode) => match mode {
|
||||
@ -412,7 +425,7 @@ impl<'a> Handler {
|
||||
.config
|
||||
.try_borrow()
|
||||
.map_err(|err| anyhow!("Could not read config: {err:?}"))?;
|
||||
let gpu_config = config.gpus.get(id);
|
||||
let gpu_config = config.gpus()?.get(id);
|
||||
|
||||
let states = self.controller_by_id(id)?.get_power_states(gpu_config);
|
||||
Ok(states)
|
||||
@ -593,6 +606,53 @@ impl<'a> Handler {
|
||||
Ok(out_path)
|
||||
}
|
||||
|
||||
pub fn list_profiles(&self) -> ProfilesInfo {
|
||||
let config = self.config.borrow();
|
||||
ProfilesInfo {
|
||||
profiles: config.profiles.keys().cloned().collect(),
|
||||
current_profile: config.current_profile.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_profile(&self, name: Option<String>) -> anyhow::Result<()> {
|
||||
if let Some(name) = &name {
|
||||
self.config.borrow().profile(name)?;
|
||||
}
|
||||
|
||||
self.cleanup().await;
|
||||
self.config.borrow_mut().current_profile = name;
|
||||
|
||||
self.apply_current_config().await?;
|
||||
self.config.borrow_mut().save()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_profile(&self, name: String, base: ProfileBase) -> anyhow::Result<()> {
|
||||
let mut config = self.config.borrow_mut();
|
||||
if config.profiles.contains_key(&name) {
|
||||
bail!("Profile {name} already exists");
|
||||
}
|
||||
|
||||
let profile = match base {
|
||||
ProfileBase::Empty => Profile::default(),
|
||||
ProfileBase::Default => config.default_profile(),
|
||||
ProfileBase::Profile(name) => config.profile(&name)?.clone(),
|
||||
};
|
||||
config.profiles.insert(name, profile);
|
||||
config.save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_profile(&self, name: String) -> anyhow::Result<()> {
|
||||
if self.config.borrow().current_profile.as_ref() == Some(&name) {
|
||||
self.set_profile(None).await?;
|
||||
}
|
||||
self.config.borrow_mut().profiles.shift_remove(&name);
|
||||
self.config.borrow().save()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn confirm_pending_config(&self, command: ConfirmCommand) -> anyhow::Result<()> {
|
||||
if let Some(tx) = self
|
||||
.confirm_config_tx
|
||||
@ -611,7 +671,7 @@ impl<'a> Handler {
|
||||
self.cleanup().await;
|
||||
|
||||
let mut config = self.config.borrow_mut();
|
||||
config.gpus.clear();
|
||||
config.clear();
|
||||
|
||||
*self.config_last_saved.lock().unwrap() = Instant::now();
|
||||
if let Err(err) = config.save() {
|
||||
|
@ -10,7 +10,9 @@ pub async fn listen_events(handler: Handler) {
|
||||
Ok(mut stream) => {
|
||||
while stream.next().await.is_some() {
|
||||
info!("suspend/resume event detected, reloading config");
|
||||
handler.apply_current_config().await;
|
||||
if let Err(err) = handler.apply_current_config().await {
|
||||
error!("could not reapply config: {err:#}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => error!("could not subscribe to suspend events: {err:#}"),
|
||||
|
@ -24,7 +24,7 @@ use gtk::{
|
||||
ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog,
|
||||
MessageType, ResponseType,
|
||||
};
|
||||
use header::Header;
|
||||
use header::{Header, HeaderMsg};
|
||||
use lact_client::DaemonClient;
|
||||
use lact_daemon::MODULE_CONF_PATH;
|
||||
use lact_schema::{
|
||||
@ -181,7 +181,7 @@ impl AsyncComponent for AppModel {
|
||||
show_embedded_info(&root, err);
|
||||
}
|
||||
|
||||
sender.input(AppMsg::ReloadData { full: true });
|
||||
sender.input(AppMsg::ReloadProfiles);
|
||||
|
||||
AsyncComponentParts { model, widgets }
|
||||
}
|
||||
@ -210,6 +210,11 @@ impl AppModel {
|
||||
) -> Result<(), Rc<anyhow::Error>> {
|
||||
match msg {
|
||||
AppMsg::Error(err) => Err(err),
|
||||
AppMsg::ReloadProfiles => {
|
||||
self.reload_profiles().await?;
|
||||
sender.input(AppMsg::ReloadData { full: false });
|
||||
Ok(())
|
||||
}
|
||||
AppMsg::ReloadData { full } => {
|
||||
let gpu_id = self.current_gpu_id()?;
|
||||
if full {
|
||||
@ -219,6 +224,24 @@ impl AppModel {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
AppMsg::SelectProfile(profile) => {
|
||||
self.daemon_client.set_profile(profile).await?;
|
||||
sender.input(AppMsg::ReloadData { full: false });
|
||||
Ok(())
|
||||
}
|
||||
AppMsg::CreateProfile(name, base) => {
|
||||
self.daemon_client
|
||||
.create_profile(name.clone(), base)
|
||||
.await?;
|
||||
self.daemon_client.set_profile(Some(name)).await?;
|
||||
sender.input(AppMsg::ReloadProfiles);
|
||||
Ok(())
|
||||
}
|
||||
AppMsg::DeleteProfile(profile) => {
|
||||
self.daemon_client.delete_profile(profile).await?;
|
||||
sender.input(AppMsg::ReloadProfiles);
|
||||
Ok(())
|
||||
}
|
||||
AppMsg::Stats(stats) => {
|
||||
self.root_stack.info_page.set_stats(&stats);
|
||||
self.root_stack.thermals_page.set_stats(&stats, false);
|
||||
@ -308,6 +331,12 @@ impl AppModel {
|
||||
.context("No GPU selected")
|
||||
}
|
||||
|
||||
async fn reload_profiles(&mut self) -> anyhow::Result<()> {
|
||||
let profiles = self.daemon_client.list_profiles().await?.inner()?;
|
||||
self.header.emit(HeaderMsg::Profiles(profiles));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_gpu_data_full(
|
||||
&mut self,
|
||||
gpu_id: String,
|
||||
@ -334,6 +363,8 @@ impl AppModel {
|
||||
|
||||
self.root_stack.thermals_page.set_info(&info);
|
||||
|
||||
self.graphs_window.clear();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -434,8 +465,6 @@ impl AppModel {
|
||||
.send(ApplyRevealerMsg::Hide)
|
||||
.unwrap();
|
||||
|
||||
self.graphs_window.clear();
|
||||
|
||||
self.stats_task_handle = Some(start_stats_update_loop(
|
||||
gpu_id.to_owned(),
|
||||
self.daemon_client.clone(),
|
@ -1,21 +1,39 @@
|
||||
mod new_profile_dialog;
|
||||
|
||||
use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow};
|
||||
use glib::clone;
|
||||
use gtk::prelude::*;
|
||||
use gtk::*;
|
||||
use lact_client::schema::DeviceListEntry;
|
||||
use lact_schema::ProfilesInfo;
|
||||
use new_profile_dialog::NewProfileDialog;
|
||||
use relm4::{
|
||||
Component, ComponentController, ComponentParts, ComponentSender, Controller, SimpleComponent,
|
||||
typed_view::list::{RelmListItem, TypedListView},
|
||||
Component, ComponentController, ComponentParts, ComponentSender, RelmWidgetExt,
|
||||
};
|
||||
use relm4_components::simple_combo_box::SimpleComboBox;
|
||||
use std::fmt;
|
||||
|
||||
pub struct Header {
|
||||
gpu_selector: Controller<SimpleComboBox<DeviceListEntry>>,
|
||||
gpu_selector: TypedListView<GpuListItem, gtk::SingleSelection>,
|
||||
profile_selector: TypedListView<ProfileListItem, gtk::SingleSelection>,
|
||||
selector_label: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum HeaderMsg {
|
||||
Profiles(ProfilesInfo),
|
||||
SelectProfile,
|
||||
SelectGpu,
|
||||
CreateProfile,
|
||||
DeleteProfile,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl SimpleComponent for Header {
|
||||
impl Component for Header {
|
||||
type Init = (Vec<DeviceListEntry>, gtk::Stack);
|
||||
type Input = ();
|
||||
type Input = HeaderMsg;
|
||||
type Output = AppMsg;
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
gtk::HeaderBar {
|
||||
@ -26,14 +44,80 @@ impl SimpleComponent for Header {
|
||||
set_stack: Some(&stack),
|
||||
},
|
||||
|
||||
#[name = "menu_button"]
|
||||
pack_start = >k::MenuButton {
|
||||
#[watch]
|
||||
set_label: &model.selector_label,
|
||||
#[wrap(Some)]
|
||||
set_popover = >k::Popover {
|
||||
set_margin_all: 5,
|
||||
set_autohide: false,
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
|
||||
gtk::Frame {
|
||||
set_label: Some("GPU"),
|
||||
set_label_align: 0.05,
|
||||
set_margin_all: 5,
|
||||
|
||||
gtk::ScrolledWindow {
|
||||
set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic),
|
||||
set_propagate_natural_height: true,
|
||||
|
||||
#[local_ref]
|
||||
pack_start = gpu_selector -> ComboBoxText,
|
||||
gpu_selector -> gtk::ListView { }
|
||||
}
|
||||
},
|
||||
|
||||
gtk::Frame {
|
||||
set_label: Some("Settings Profile"),
|
||||
set_label_align: 0.05,
|
||||
set_margin_all: 5,
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
|
||||
gtk::ScrolledWindow {
|
||||
set_policy: (gtk::PolicyType::Never, gtk::PolicyType::Automatic),
|
||||
set_propagate_natural_height: true,
|
||||
|
||||
#[local_ref]
|
||||
profile_selector -> gtk::ListView { }
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
|
||||
gtk::Button {
|
||||
set_expand: true,
|
||||
set_icon_name: "list-add-symbolic",
|
||||
connect_clicked => HeaderMsg::CreateProfile,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_expand: true,
|
||||
set_icon_name: "list-remove-symbolic",
|
||||
connect_clicked => HeaderMsg::DeleteProfile,
|
||||
#[watch]
|
||||
set_sensitive: model.profile_selector.selection_model.selected() != 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
pack_end = >k::MenuButton {
|
||||
set_icon_name: "open-menu-symbolic",
|
||||
set_menu_model: Some(&app_menu),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
menu! {
|
||||
@ -57,114 +141,225 @@ impl SimpleComponent for Header {
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let gpu_selector = SimpleComboBox::builder()
|
||||
.launch(SimpleComboBox {
|
||||
variants,
|
||||
active_index: Some(0),
|
||||
})
|
||||
.forward(sender.output_sender(), |_| AppMsg::ReloadData {
|
||||
full: true,
|
||||
});
|
||||
sender.input(HeaderMsg::SelectGpu);
|
||||
|
||||
// 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 mut gpu_selector = TypedListView::<_, gtk::SingleSelection>::new();
|
||||
gpu_selector.extend_from_iter(variants.into_iter().map(GpuListItem));
|
||||
|
||||
gpu_selector
|
||||
.selection_model
|
||||
.connect_selection_changed(clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
move |_, _, _| {
|
||||
sender.input(HeaderMsg::SelectGpu);
|
||||
}
|
||||
));
|
||||
|
||||
let model = Self { gpu_selector };
|
||||
let profile_selector = TypedListView::<_, gtk::SingleSelection>::new();
|
||||
profile_selector
|
||||
.selection_model
|
||||
.connect_selection_changed(clone!(
|
||||
#[strong]
|
||||
sender,
|
||||
move |_, _, _| {
|
||||
sender.input(HeaderMsg::SelectProfile);
|
||||
}
|
||||
));
|
||||
|
||||
let gpu_selector = model.gpu_selector.widget();
|
||||
let model = Self {
|
||||
gpu_selector,
|
||||
profile_selector,
|
||||
selector_label: String::new(),
|
||||
};
|
||||
|
||||
let gpu_selector = &model.gpu_selector.view;
|
||||
let profile_selector = &model.profile_selector.view;
|
||||
let widgets = view_output!();
|
||||
|
||||
widgets.menu_button.connect_label_notify(|menu_button| {
|
||||
let label_box = menu_button
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.downcast::<gtk::ToggleButton>()
|
||||
.unwrap()
|
||||
.child()
|
||||
.unwrap()
|
||||
.downcast::<gtk::Box>()
|
||||
.unwrap();
|
||||
// Limits the length of text in the menu button
|
||||
let selector_label = label_box
|
||||
.first_child()
|
||||
.unwrap()
|
||||
.downcast::<Label>()
|
||||
.unwrap();
|
||||
selector_label.set_ellipsize(pango::EllipsizeMode::End);
|
||||
selector_label.set_width_chars(14);
|
||||
});
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update_with_view(
|
||||
&mut self,
|
||||
widgets: &mut Self::Widgets,
|
||||
msg: Self::Input,
|
||||
sender: ComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
match msg {
|
||||
HeaderMsg::Profiles(profiles_info) => {
|
||||
let selected_index = match &profiles_info.current_profile {
|
||||
Some(profile) => {
|
||||
profiles_info
|
||||
.profiles
|
||||
.iter()
|
||||
.position(|value| value == profile)
|
||||
.expect("Active profile is not in the list")
|
||||
+ 1
|
||||
}
|
||||
None => 0,
|
||||
};
|
||||
|
||||
self.profile_selector.clear();
|
||||
self.profile_selector.append(ProfileListItem::Default);
|
||||
|
||||
for profile in profiles_info.profiles {
|
||||
self.profile_selector
|
||||
.append(ProfileListItem::Profile(profile));
|
||||
}
|
||||
|
||||
self.profile_selector
|
||||
.selection_model
|
||||
.set_selected(selected_index as u32);
|
||||
}
|
||||
HeaderMsg::SelectGpu => sender.output(AppMsg::ReloadData { full: true }).unwrap(),
|
||||
HeaderMsg::SelectProfile => {
|
||||
let selected_profile = self.selected_profile();
|
||||
sender
|
||||
.output(AppMsg::SelectProfile(selected_profile))
|
||||
.unwrap();
|
||||
}
|
||||
HeaderMsg::CreateProfile => {
|
||||
let mut diag_controller = NewProfileDialog::builder()
|
||||
.launch(self.custom_profiles())
|
||||
.forward(sender.output_sender(), |(name, base)| {
|
||||
AppMsg::CreateProfile(name, base)
|
||||
});
|
||||
diag_controller.detach_runtime();
|
||||
}
|
||||
HeaderMsg::DeleteProfile => {
|
||||
if let Some(selected_profile) = self.selected_profile() {
|
||||
let msg =
|
||||
format!("Are you sure you want to delete profile \"{selected_profile}\"");
|
||||
sender
|
||||
.output(AppMsg::ask_confirmation(
|
||||
AppMsg::DeleteProfile(selected_profile),
|
||||
"Delete profile",
|
||||
&msg,
|
||||
gtk::ButtonsType::OkCancel,
|
||||
))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.update_label();
|
||||
|
||||
self.update_view(widgets, sender);
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub fn selected_gpu_id(&self) -> Option<String> {
|
||||
let selected = self.gpu_selector.selection_model.selected();
|
||||
self.gpu_selector
|
||||
.model()
|
||||
.get_active_elem()
|
||||
.map(|model| model.id.clone())
|
||||
.get(selected)
|
||||
.as_ref()
|
||||
.map(|item| item.borrow().0.id.clone())
|
||||
}
|
||||
|
||||
fn custom_profiles(&self) -> Vec<String> {
|
||||
let mut profiles = Vec::with_capacity(self.profile_selector.len() as usize);
|
||||
for i in 0..self.profile_selector.len() {
|
||||
let item = self.profile_selector.get(i).unwrap();
|
||||
let item = item.borrow();
|
||||
if let ProfileListItem::Profile(name) = &*item {
|
||||
profiles.push(name.clone());
|
||||
}
|
||||
}
|
||||
profiles
|
||||
}
|
||||
|
||||
fn selected_profile(&self) -> Option<String> {
|
||||
let selected_index = self.profile_selector.selection_model.selected();
|
||||
let item = self
|
||||
.profile_selector
|
||||
.get(selected_index)
|
||||
.expect("Invalid item selected");
|
||||
let item = item.borrow().clone();
|
||||
match item {
|
||||
ProfileListItem::Default => None,
|
||||
ProfileListItem::Profile(name) => Some(name),
|
||||
}
|
||||
}
|
||||
|
||||
/*#[derive(Clone)]
|
||||
pub struct Header {
|
||||
pub container: HeaderBar,
|
||||
gpu_selector: ComboBoxText,
|
||||
switcher: StackSwitcher,
|
||||
}
|
||||
fn update_label(&mut self) {
|
||||
let gpu_index = self.gpu_selector.selection_model.selected();
|
||||
let profile = self
|
||||
.profile_selector
|
||||
.get(self.profile_selector.selection_model.selected())
|
||||
.as_ref()
|
||||
.map(|item| item.borrow().to_string())
|
||||
.unwrap_or_else(|| "<Unknown>".to_owned());
|
||||
|
||||
impl Header {
|
||||
pub fn new(system_info: &SystemInfo) -> Self {
|
||||
let container = HeaderBar::new();
|
||||
container.set_show_title_buttons(true);
|
||||
|
||||
let switcher = StackSwitcher::new();
|
||||
container.set_title_widget(Some(&switcher));
|
||||
|
||||
let gpu_selector = ComboBoxText::new();
|
||||
container.pack_start(&gpu_selector);
|
||||
|
||||
let menu = gio::Menu::new();
|
||||
menu.append(
|
||||
Some("Show historical charts"),
|
||||
Some("app.show-graphs-window"),
|
||||
);
|
||||
menu.append(Some("Dump VBIOS"), Some("app.dump-vbios"));
|
||||
menu.append(
|
||||
Some("Generate debug snapshot"),
|
||||
Some("app.generate-debug-snapshot"),
|
||||
);
|
||||
|
||||
if system_info.amdgpu_overdrive_enabled == Some(true) {
|
||||
menu.append(
|
||||
Some("Disable overclocking support"),
|
||||
Some("app.disable-overdrive"),
|
||||
)
|
||||
}
|
||||
|
||||
menu.append(Some("Reset all configuration"), Some("app.reset-config"));
|
||||
|
||||
let menu_button = MenuButton::builder()
|
||||
.icon_name("open-menu-symbolic")
|
||||
.menu_model(&menu)
|
||||
.build();
|
||||
container.pack_end(&menu_button);
|
||||
|
||||
Header {
|
||||
container,
|
||||
gpu_selector,
|
||||
switcher,
|
||||
self.selector_label = format!("GPU {gpu_index} | {profile}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_switcher_stack(&self, stack: &Stack) {
|
||||
self.switcher.set_stack(Some(stack));
|
||||
struct GpuListItem(DeviceListEntry);
|
||||
|
||||
impl RelmListItem for GpuListItem {
|
||||
type Root = gtk::Label;
|
||||
type Widgets = gtk::Label;
|
||||
|
||||
fn setup(_list_item: >k::ListItem) -> (Self::Root, Self::Widgets) {
|
||||
let label = gtk::Label::new(None);
|
||||
label.set_margin_all(5);
|
||||
(label.clone(), label)
|
||||
}
|
||||
|
||||
pub fn set_devices(&self, gpus: &[DeviceListEntry<'_>]) {
|
||||
for (i, entry) in gpus.iter().enumerate() {
|
||||
let name = format!("{i}: {}", entry.name.unwrap_or_default());
|
||||
self.gpu_selector.append(Some(entry.id), &name);
|
||||
fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
|
||||
widgets.set_label(self.0.name.as_deref().unwrap_or(&self.0.id));
|
||||
}
|
||||
}
|
||||
|
||||
//limits the length of gpu names in combobox
|
||||
for cell in self.gpu_selector.cells() {
|
||||
cell.set_property("width-chars", 10);
|
||||
cell.set_property("ellipsize", EllipsizeMode::End);
|
||||
#[derive(Clone)]
|
||||
enum ProfileListItem {
|
||||
Default,
|
||||
Profile(String),
|
||||
}
|
||||
|
||||
self.gpu_selector.set_active(Some(0));
|
||||
impl fmt::Display for ProfileListItem {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
ProfileListItem::Default => "Default",
|
||||
ProfileListItem::Profile(name) => name,
|
||||
};
|
||||
text.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect_gpu_selection_changed<F: Fn(String) + 'static>(&self, f: F) {
|
||||
self.gpu_selector.connect_changed(move |gpu_selector| {
|
||||
if let Some(selected_id) = gpu_selector.active_id() {
|
||||
f(selected_id.to_string());
|
||||
impl RelmListItem for ProfileListItem {
|
||||
type Root = Label;
|
||||
type Widgets = Label;
|
||||
|
||||
fn setup(_list_item: >k::ListItem) -> (Self::Root, Self::Widgets) {
|
||||
let label = gtk::Label::new(None);
|
||||
label.set_margin_all(5);
|
||||
(label.clone(), label)
|
||||
}
|
||||
|
||||
fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
|
||||
widgets.set_label(&self.to_string());
|
||||
}
|
||||
});
|
||||
}
|
||||
}*/
|
||||
|
129
lact-gui/src/app/header/new_profile_dialog.rs
Normal file
129
lact-gui/src/app/header/new_profile_dialog.rs
Normal file
@ -0,0 +1,129 @@
|
||||
use gtk::prelude::*;
|
||||
use lact_schema::request::ProfileBase;
|
||||
use relm4::{
|
||||
Component, ComponentController, ComponentParts, ComponentSender, Controller, RelmWidgetExt,
|
||||
};
|
||||
use relm4_components::simple_combo_box::SimpleComboBox;
|
||||
|
||||
pub struct NewProfileDialog {
|
||||
name_buffer: gtk::EntryBuffer,
|
||||
base_selector: Controller<SimpleComboBox<ProfileBase>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum NewProfileDialogMsg {
|
||||
Create,
|
||||
}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl Component for NewProfileDialog {
|
||||
type Init = Vec<String>;
|
||||
type Input = NewProfileDialogMsg;
|
||||
type Output = (String, ProfileBase);
|
||||
type CommandOutput = ();
|
||||
|
||||
view! {
|
||||
gtk::Window {
|
||||
set_default_size: (250, 130),
|
||||
set_title: Some("Create Profile"),
|
||||
set_hide_on_close: true,
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
set_margin_all: 10,
|
||||
|
||||
gtk::Entry {
|
||||
set_placeholder_text: Some("Name"),
|
||||
set_buffer: &model.name_buffer,
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
|
||||
gtk::Label {
|
||||
set_label: "Base profile:",
|
||||
},
|
||||
|
||||
#[local_ref]
|
||||
base_selector -> gtk::ComboBoxText {
|
||||
set_margin_horizontal: 5,
|
||||
set_hexpand: true,
|
||||
set_halign: gtk::Align::End,
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_spacing: 5,
|
||||
set_hexpand: true,
|
||||
set_vexpand: true,
|
||||
set_valign: gtk::Align::End,
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Cancel",
|
||||
set_hexpand: true,
|
||||
|
||||
connect_clicked[root] => move |_| {
|
||||
root.hide();
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Create",
|
||||
set_hexpand: true,
|
||||
|
||||
connect_clicked => NewProfileDialogMsg::Create,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn init(
|
||||
current_profiles: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let mut variants = vec![ProfileBase::Empty, ProfileBase::Default];
|
||||
variants.extend(current_profiles.into_iter().map(ProfileBase::Profile));
|
||||
|
||||
let base_selector = SimpleComboBox::<ProfileBase>::builder()
|
||||
.launch(SimpleComboBox {
|
||||
variants,
|
||||
active_index: Some(1),
|
||||
})
|
||||
.detach();
|
||||
|
||||
let model = Self {
|
||||
base_selector,
|
||||
name_buffer: gtk::EntryBuffer::default(),
|
||||
};
|
||||
|
||||
let base_selector = model.base_selector.widget();
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
root.present();
|
||||
|
||||
ComponentParts { model, widgets }
|
||||
}
|
||||
|
||||
fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>, root: &Self::Root) {
|
||||
match msg {
|
||||
NewProfileDialogMsg::Create => {
|
||||
if self.name_buffer.length() != 0 {
|
||||
if let Some(selected) = self.base_selector.model().active_index {
|
||||
let base = self.base_selector.model().variants[selected].clone();
|
||||
sender
|
||||
.output((self.name_buffer.text().to_string(), base))
|
||||
.unwrap();
|
||||
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
use super::confirmation_dialog::ConfirmationOptions;
|
||||
use lact_schema::DeviceStats;
|
||||
use lact_schema::{request::ProfileBase, DeviceStats};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@ -17,6 +17,10 @@ pub enum AppMsg {
|
||||
EnableOverdrive,
|
||||
DisableOverdrive,
|
||||
ResetConfig,
|
||||
ReloadProfiles,
|
||||
SelectProfile(Option<String>),
|
||||
CreateProfile(String, ProfileBase),
|
||||
DeleteProfile(String),
|
||||
AskConfirmation(ConfirmationOptions, Box<AppMsg>),
|
||||
}
|
||||
|
||||
|
@ -10,10 +10,10 @@ args = ["clap"]
|
||||
amdgpu-sysfs = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_with = { workspace = true }
|
||||
serde-error = "=0.1.2"
|
||||
anyhow = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
|
||||
indexmap = { version = "*", features = ["serde"] }
|
||||
serde-error = "=0.1.2"
|
||||
clap = { version = "4.4.18", features = ["derive"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
|
@ -306,3 +306,9 @@ pub struct FanOptions<'a> {
|
||||
pub spindown_delay_ms: Option<u64>,
|
||||
pub change_threshold: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
|
||||
pub struct ProfilesInfo {
|
||||
pub profiles: Vec<String>,
|
||||
pub current_profile: Option<String>,
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::FanOptions;
|
||||
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -57,6 +59,17 @@ pub enum Request<'a> {
|
||||
VbiosDump {
|
||||
id: &'a str,
|
||||
},
|
||||
ListProfiles,
|
||||
SetProfile {
|
||||
name: Option<String>,
|
||||
},
|
||||
CreateProfile {
|
||||
name: String,
|
||||
base: ProfileBase,
|
||||
},
|
||||
DeleteProfile {
|
||||
name: String,
|
||||
},
|
||||
EnableOverdrive,
|
||||
DisableOverdrive,
|
||||
GenerateSnapshot,
|
||||
@ -83,3 +96,22 @@ pub enum SetClocksCommand {
|
||||
VoltageOffset(i32),
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ProfileBase {
|
||||
Empty,
|
||||
Default,
|
||||
Profile(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for ProfileBase {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let text = match self {
|
||||
ProfileBase::Empty => "Empty",
|
||||
ProfileBase::Default => "Default",
|
||||
ProfileBase::Profile(name) => name,
|
||||
};
|
||||
text.fmt(f)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user