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]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.4.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
|
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
@ -1611,6 +1611,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
"futures",
|
"futures",
|
||||||
|
"indexmap",
|
||||||
"lact-schema",
|
"lact-schema",
|
||||||
"libdrm_amdgpu_sys",
|
"libdrm_amdgpu_sys",
|
||||||
"libflate",
|
"libflate",
|
||||||
|
@ -23,6 +23,7 @@ futures = { version = "0.3.30", default-features = false }
|
|||||||
tokio = { version = "1.35.1", default-features = false }
|
tokio = { version = "1.35.1", default-features = false }
|
||||||
nix = { version = "0.29.0", default-features = false }
|
nix = { version = "0.29.0", default-features = false }
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
|
indexmap = { version = "2.5.0", features = ["serde"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = "symbols"
|
strip = "symbols"
|
||||||
|
@ -3,6 +3,7 @@ mod connection;
|
|||||||
mod macros;
|
mod macros;
|
||||||
|
|
||||||
pub use lact_schema as schema;
|
pub use lact_schema as schema;
|
||||||
|
use lact_schema::request::ProfileBase;
|
||||||
|
|
||||||
use amdgpu_sysfs::gpu_handle::{
|
use amdgpu_sysfs::gpu_handle::{
|
||||||
power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind,
|
power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind,
|
||||||
@ -12,8 +13,8 @@ use connection::{tcp::TcpConnection, unix::UnixConnection, DaemonConnection};
|
|||||||
use nix::unistd::getuid;
|
use nix::unistd::getuid;
|
||||||
use schema::{
|
use schema::{
|
||||||
request::{ConfirmCommand, SetClocksCommand},
|
request::{ConfirmCommand, SetClocksCommand},
|
||||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, Request,
|
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanOptions, PowerStates, ProfilesInfo,
|
||||||
Response, SystemInfo,
|
Request, Response, SystemInfo,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{
|
use std::{
|
||||||
@ -116,6 +117,7 @@ impl DaemonClient {
|
|||||||
request_plain!(disable_overdrive, DisableOverdrive, String);
|
request_plain!(disable_overdrive, DisableOverdrive, String);
|
||||||
request_plain!(generate_debug_snapshot, GenerateSnapshot, String);
|
request_plain!(generate_debug_snapshot, GenerateSnapshot, String);
|
||||||
request_plain!(reset_config, RestConfig, ());
|
request_plain!(reset_config, RestConfig, ());
|
||||||
|
request_plain!(list_profiles, ListProfiles, ProfilesInfo);
|
||||||
request_with_id!(get_device_info, DeviceInfo, DeviceInfo);
|
request_with_id!(get_device_info, DeviceInfo, DeviceInfo);
|
||||||
request_with_id!(get_device_stats, DeviceStats, DeviceStats);
|
request_with_id!(get_device_stats, DeviceStats, DeviceStats);
|
||||||
request_with_id!(get_device_clocks_info, DeviceClocksInfo, ClocksInfo);
|
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!(reset_pmfw, ResetPmfw, u64);
|
||||||
request_with_id!(dump_vbios, VbiosDump, Vec<u8>);
|
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(
|
pub async fn set_performance_level(
|
||||||
&self,
|
&self,
|
||||||
id: &str,
|
id: &str,
|
||||||
|
@ -27,6 +27,7 @@ tokio = { workspace = true, features = [
|
|||||||
"sync",
|
"sync",
|
||||||
] }
|
] }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
|
|
||||||
pciid-parser = { version = "0.7", features = ["serde"] }
|
pciid-parser = { version = "0.7", features = ["serde"] }
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::server::gpu_controller::fan_control::FanCurve;
|
use crate::server::gpu_controller::fan_control::FanCurve;
|
||||||
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use indexmap::IndexMap;
|
||||||
use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions};
|
use lact_schema::{default_fan_curve, request::SetClocksCommand, FanControlMode, PmfwOptions};
|
||||||
use nix::unistd::getuid;
|
use nix::unistd::getuid;
|
||||||
use notify::{RecommendedWatcher, Watcher};
|
use notify::{RecommendedWatcher, Watcher};
|
||||||
@ -27,7 +28,11 @@ pub struct Config {
|
|||||||
#[serde(default = "default_apply_settings_timer")]
|
#[serde(default = "default_apply_settings_timer")]
|
||||||
pub apply_settings_timer: u64,
|
pub apply_settings_timer: u64,
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
#[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 {
|
impl Default for Config {
|
||||||
@ -36,6 +41,8 @@ impl Default for Config {
|
|||||||
daemon: Daemon::default(),
|
daemon: Daemon::default(),
|
||||||
apply_settings_timer: default_apply_settings_timer(),
|
apply_settings_timer: default_apply_settings_timer(),
|
||||||
gpus: HashMap::new(),
|
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]
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||||
pub struct Gpu {
|
pub struct Gpu {
|
||||||
@ -178,6 +191,54 @@ impl Config {
|
|||||||
Ok(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> {
|
pub fn start_watcher(config_last_saved: Arc<Mutex<Instant>>) -> mpsc::UnboundedReceiver<Config> {
|
||||||
|
@ -18,7 +18,7 @@ use tokio::{
|
|||||||
signal::unix::{signal, SignalKind},
|
signal::unix::{signal, SignalKind},
|
||||||
task::LocalSet,
|
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
|
/// RDNA3, minimum family that supports the new pmfw interface
|
||||||
pub const AMDGPU_FAMILY_GC_11_0_0: u32 = 145;
|
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 {
|
while let Some(new_config) = rx.recv().await {
|
||||||
info!("config file was changed, reloading");
|
info!("config file was changed, reloading");
|
||||||
handler.config.replace(new_config);
|
handler.config.replace(new_config);
|
||||||
handler.apply_current_config().await;
|
match handler.apply_current_config().await {
|
||||||
|
Ok(()) => {
|
||||||
info!("configuration reloaded");
|
info!("configuration reloaded");
|
||||||
}
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("could not apply new config: {err:#}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_sufficient_uptime() {
|
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?)
|
ok_response(handler.set_enabled_power_states(id, kind, states).await?)
|
||||||
}
|
}
|
||||||
Request::VbiosDump { id } => ok_response(handler.vbios_dump(id)?),
|
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::EnableOverdrive => ok_response(system::enable_overdrive().await?),
|
||||||
Request::DisableOverdrive => ok_response(system::disable_overdrive().await?),
|
Request::DisableOverdrive => ok_response(system::disable_overdrive().await?),
|
||||||
Request::GenerateSnapshot => ok_response(handler.generate_snapshot().await?),
|
Request::GenerateSnapshot => ok_response(handler.generate_snapshot().await?),
|
@ -2,17 +2,17 @@ use super::{
|
|||||||
gpu_controller::{fan_control::FanCurve, GpuController},
|
gpu_controller::{fan_control::FanCurve, GpuController},
|
||||||
system::{self, detect_initramfs_type, PP_FEATURE_MASK_PATH},
|
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::{
|
use amdgpu_sysfs::{
|
||||||
gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind},
|
gpu_handle::{power_profile_mode::PowerProfileModesTable, PerformanceLevel, PowerLevelKind},
|
||||||
sysfs::SysFS,
|
sysfs::SysFS,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, bail, Context};
|
||||||
use lact_schema::{
|
use lact_schema::{
|
||||||
default_fan_curve,
|
default_fan_curve,
|
||||||
request::{ConfirmCommand, SetClocksCommand},
|
request::{ConfirmCommand, ProfileBase, SetClocksCommand},
|
||||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions,
|
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions,
|
||||||
PowerStates,
|
PowerStates, ProfilesInfo,
|
||||||
};
|
};
|
||||||
use libflate::gzip;
|
use libflate::gzip;
|
||||||
use nix::libc;
|
use nix::libc;
|
||||||
@ -135,7 +135,9 @@ impl<'a> Handler {
|
|||||||
confirm_config_tx: Rc::new(RefCell::new(None)),
|
confirm_config_tx: Rc::new(RefCell::new(None)),
|
||||||
config_last_saved: Arc::new(Mutex::new(Instant::now())),
|
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
|
// Eagerly release memory
|
||||||
// `load_controllers` allocates and deallocates the entire PCI ID database,
|
// `load_controllers` allocates and deallocates the entire PCI ID database,
|
||||||
@ -148,10 +150,11 @@ impl<'a> Handler {
|
|||||||
Ok(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
|
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 Some(controller) = self.gpu_controllers.get(id) {
|
||||||
if let Err(err) = controller.apply_config(gpu_config).await {
|
if let Err(err) = controller.apply_config(gpu_config).await {
|
||||||
error!("could not apply existing config for gpu {id}: {err}");
|
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");
|
info!("could not find GPU with id {id} defined in configuration");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn edit_gpu_config<F: FnOnce(&mut config::Gpu)>(
|
async fn edit_gpu_config<F: FnOnce(&mut config::Gpu)>(
|
||||||
@ -181,7 +186,7 @@ impl<'a> Handler {
|
|||||||
let (gpu_config, apply_timer) = {
|
let (gpu_config, apply_timer) = {
|
||||||
let config = self.config.try_borrow().map_err(|err| anyhow!("{err}"))?;
|
let config = self.config.try_borrow().map_err(|err| anyhow!("{err}"))?;
|
||||||
let apply_timer = config.apply_settings_timer;
|
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)
|
(gpu_config, apply_timer)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -243,7 +248,12 @@ impl<'a> Handler {
|
|||||||
*handler.config_last_saved.lock().unwrap() = Instant::now();
|
*handler.config_last_saved.lock().unwrap() = Instant::now();
|
||||||
|
|
||||||
let mut config_guard = handler.config.borrow_mut();
|
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() {
|
if let Err(err) = config_guard.save() {
|
||||||
error!("{err:#}");
|
error!("{err:#}");
|
||||||
@ -302,7 +312,7 @@ impl<'a> Handler {
|
|||||||
.config
|
.config
|
||||||
.try_borrow()
|
.try_borrow()
|
||||||
.map_err(|err| anyhow!("Could not read config: {err:?}"))?;
|
.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))
|
Ok(self.controller_by_id(id)?.get_stats(gpu_config))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,7 +326,10 @@ impl<'a> Handler {
|
|||||||
.config
|
.config
|
||||||
.try_borrow_mut()
|
.try_borrow_mut()
|
||||||
.map_err(|err| anyhow!("{err}"))?;
|
.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 {
|
match opts.mode {
|
||||||
Some(mode) => match mode {
|
Some(mode) => match mode {
|
||||||
@ -412,7 +425,7 @@ impl<'a> Handler {
|
|||||||
.config
|
.config
|
||||||
.try_borrow()
|
.try_borrow()
|
||||||
.map_err(|err| anyhow!("Could not read config: {err:?}"))?;
|
.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);
|
let states = self.controller_by_id(id)?.get_power_states(gpu_config);
|
||||||
Ok(states)
|
Ok(states)
|
||||||
@ -593,6 +606,53 @@ impl<'a> Handler {
|
|||||||
Ok(out_path)
|
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<()> {
|
pub fn confirm_pending_config(&self, command: ConfirmCommand) -> anyhow::Result<()> {
|
||||||
if let Some(tx) = self
|
if let Some(tx) = self
|
||||||
.confirm_config_tx
|
.confirm_config_tx
|
||||||
@ -611,7 +671,7 @@ impl<'a> Handler {
|
|||||||
self.cleanup().await;
|
self.cleanup().await;
|
||||||
|
|
||||||
let mut config = self.config.borrow_mut();
|
let mut config = self.config.borrow_mut();
|
||||||
config.gpus.clear();
|
config.clear();
|
||||||
|
|
||||||
*self.config_last_saved.lock().unwrap() = Instant::now();
|
*self.config_last_saved.lock().unwrap() = Instant::now();
|
||||||
if let Err(err) = config.save() {
|
if let Err(err) = config.save() {
|
||||||
|
@ -10,7 +10,9 @@ pub async fn listen_events(handler: Handler) {
|
|||||||
Ok(mut stream) => {
|
Ok(mut stream) => {
|
||||||
while stream.next().await.is_some() {
|
while stream.next().await.is_some() {
|
||||||
info!("suspend/resume event detected, reloading config");
|
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:#}"),
|
Err(err) => error!("could not subscribe to suspend events: {err:#}"),
|
||||||
|
@ -24,7 +24,7 @@ use gtk::{
|
|||||||
ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog,
|
ApplicationWindow, ButtonsType, FileChooserAction, FileChooserDialog, MessageDialog,
|
||||||
MessageType, ResponseType,
|
MessageType, ResponseType,
|
||||||
};
|
};
|
||||||
use header::Header;
|
use header::{Header, HeaderMsg};
|
||||||
use lact_client::DaemonClient;
|
use lact_client::DaemonClient;
|
||||||
use lact_daemon::MODULE_CONF_PATH;
|
use lact_daemon::MODULE_CONF_PATH;
|
||||||
use lact_schema::{
|
use lact_schema::{
|
||||||
@ -181,7 +181,7 @@ impl AsyncComponent for AppModel {
|
|||||||
show_embedded_info(&root, err);
|
show_embedded_info(&root, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
sender.input(AppMsg::ReloadData { full: true });
|
sender.input(AppMsg::ReloadProfiles);
|
||||||
|
|
||||||
AsyncComponentParts { model, widgets }
|
AsyncComponentParts { model, widgets }
|
||||||
}
|
}
|
||||||
@ -210,6 +210,11 @@ impl AppModel {
|
|||||||
) -> Result<(), Rc<anyhow::Error>> {
|
) -> Result<(), Rc<anyhow::Error>> {
|
||||||
match msg {
|
match msg {
|
||||||
AppMsg::Error(err) => Err(err),
|
AppMsg::Error(err) => Err(err),
|
||||||
|
AppMsg::ReloadProfiles => {
|
||||||
|
self.reload_profiles().await?;
|
||||||
|
sender.input(AppMsg::ReloadData { full: false });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
AppMsg::ReloadData { full } => {
|
AppMsg::ReloadData { full } => {
|
||||||
let gpu_id = self.current_gpu_id()?;
|
let gpu_id = self.current_gpu_id()?;
|
||||||
if full {
|
if full {
|
||||||
@ -219,6 +224,24 @@ impl AppModel {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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) => {
|
AppMsg::Stats(stats) => {
|
||||||
self.root_stack.info_page.set_stats(&stats);
|
self.root_stack.info_page.set_stats(&stats);
|
||||||
self.root_stack.thermals_page.set_stats(&stats, false);
|
self.root_stack.thermals_page.set_stats(&stats, false);
|
||||||
@ -308,6 +331,12 @@ impl AppModel {
|
|||||||
.context("No GPU selected")
|
.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(
|
async fn update_gpu_data_full(
|
||||||
&mut self,
|
&mut self,
|
||||||
gpu_id: String,
|
gpu_id: String,
|
||||||
@ -334,6 +363,8 @@ impl AppModel {
|
|||||||
|
|
||||||
self.root_stack.thermals_page.set_info(&info);
|
self.root_stack.thermals_page.set_info(&info);
|
||||||
|
|
||||||
|
self.graphs_window.clear();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,8 +465,6 @@ impl AppModel {
|
|||||||
.send(ApplyRevealerMsg::Hide)
|
.send(ApplyRevealerMsg::Hide)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
self.graphs_window.clear();
|
|
||||||
|
|
||||||
self.stats_task_handle = Some(start_stats_update_loop(
|
self.stats_task_handle = Some(start_stats_update_loop(
|
||||||
gpu_id.to_owned(),
|
gpu_id.to_owned(),
|
||||||
self.daemon_client.clone(),
|
self.daemon_client.clone(),
|
@ -1,21 +1,39 @@
|
|||||||
|
mod new_profile_dialog;
|
||||||
|
|
||||||
use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow};
|
use super::{AppMsg, DebugSnapshot, DisableOverdrive, DumpVBios, ResetConfig, ShowGraphsWindow};
|
||||||
|
use glib::clone;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::*;
|
use gtk::*;
|
||||||
use lact_client::schema::DeviceListEntry;
|
use lact_client::schema::DeviceListEntry;
|
||||||
|
use lact_schema::ProfilesInfo;
|
||||||
|
use new_profile_dialog::NewProfileDialog;
|
||||||
use relm4::{
|
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 {
|
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)]
|
#[relm4::component(pub)]
|
||||||
impl SimpleComponent for Header {
|
impl Component for Header {
|
||||||
type Init = (Vec<DeviceListEntry>, gtk::Stack);
|
type Init = (Vec<DeviceListEntry>, gtk::Stack);
|
||||||
type Input = ();
|
type Input = HeaderMsg;
|
||||||
type Output = AppMsg;
|
type Output = AppMsg;
|
||||||
|
type CommandOutput = ();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
gtk::HeaderBar {
|
gtk::HeaderBar {
|
||||||
@ -26,14 +44,80 @@ impl SimpleComponent for Header {
|
|||||||
set_stack: Some(&stack),
|
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]
|
#[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 {
|
pack_end = >k::MenuButton {
|
||||||
set_icon_name: "open-menu-symbolic",
|
set_icon_name: "open-menu-symbolic",
|
||||||
set_menu_model: Some(&app_menu),
|
set_menu_model: Some(&app_menu),
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
menu! {
|
menu! {
|
||||||
@ -57,114 +141,225 @@ impl SimpleComponent for Header {
|
|||||||
root: Self::Root,
|
root: Self::Root,
|
||||||
sender: ComponentSender<Self>,
|
sender: ComponentSender<Self>,
|
||||||
) -> ComponentParts<Self> {
|
) -> ComponentParts<Self> {
|
||||||
let gpu_selector = SimpleComboBox::builder()
|
sender.input(HeaderMsg::SelectGpu);
|
||||||
.launch(SimpleComboBox {
|
|
||||||
variants,
|
|
||||||
active_index: Some(0),
|
|
||||||
})
|
|
||||||
.forward(sender.output_sender(), |_| AppMsg::ReloadData {
|
|
||||||
full: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// limits the length of gpu names in combobox
|
let mut gpu_selector = TypedListView::<_, gtk::SingleSelection>::new();
|
||||||
for cell in gpu_selector.widget().cells() {
|
gpu_selector.extend_from_iter(variants.into_iter().map(GpuListItem));
|
||||||
cell.set_property("width-chars", 10);
|
|
||||||
cell.set_property("ellipsize", pango::EllipsizeMode::End);
|
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!();
|
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 }
|
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 {
|
impl Header {
|
||||||
pub fn selected_gpu_id(&self) -> Option<String> {
|
pub fn selected_gpu_id(&self) -> Option<String> {
|
||||||
|
let selected = self.gpu_selector.selection_model.selected();
|
||||||
self.gpu_selector
|
self.gpu_selector
|
||||||
.model()
|
.get(selected)
|
||||||
.get_active_elem()
|
.as_ref()
|
||||||
.map(|model| model.id.clone())
|
.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)]
|
fn update_label(&mut self) {
|
||||||
pub struct Header {
|
let gpu_index = self.gpu_selector.selection_model.selected();
|
||||||
pub container: HeaderBar,
|
let profile = self
|
||||||
gpu_selector: ComboBoxText,
|
.profile_selector
|
||||||
switcher: StackSwitcher,
|
.get(self.profile_selector.selection_model.selected())
|
||||||
}
|
.as_ref()
|
||||||
|
.map(|item| item.borrow().to_string())
|
||||||
|
.unwrap_or_else(|| "<Unknown>".to_owned());
|
||||||
|
|
||||||
impl Header {
|
self.selector_label = format!("GPU {gpu_index} | {profile}");
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_switcher_stack(&self, stack: &Stack) {
|
struct GpuListItem(DeviceListEntry);
|
||||||
self.switcher.set_stack(Some(stack));
|
|
||||||
|
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<'_>]) {
|
fn bind(&mut self, widgets: &mut Self::Widgets, _root: &mut Self::Root) {
|
||||||
for (i, entry) in gpus.iter().enumerate() {
|
widgets.set_label(self.0.name.as_deref().unwrap_or(&self.0.id));
|
||||||
let name = format!("{i}: {}", entry.name.unwrap_or_default());
|
}
|
||||||
self.gpu_selector.append(Some(entry.id), &name);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//limits the length of gpu names in combobox
|
#[derive(Clone)]
|
||||||
for cell in self.gpu_selector.cells() {
|
enum ProfileListItem {
|
||||||
cell.set_property("width-chars", 10);
|
Default,
|
||||||
cell.set_property("ellipsize", EllipsizeMode::End);
|
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) {
|
impl RelmListItem for ProfileListItem {
|
||||||
self.gpu_selector.connect_changed(move |gpu_selector| {
|
type Root = Label;
|
||||||
if let Some(selected_id) = gpu_selector.active_id() {
|
type Widgets = Label;
|
||||||
f(selected_id.to_string());
|
|
||||||
|
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 super::confirmation_dialog::ConfirmationOptions;
|
||||||
use lact_schema::DeviceStats;
|
use lact_schema::{request::ProfileBase, DeviceStats};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@ -17,6 +17,10 @@ pub enum AppMsg {
|
|||||||
EnableOverdrive,
|
EnableOverdrive,
|
||||||
DisableOverdrive,
|
DisableOverdrive,
|
||||||
ResetConfig,
|
ResetConfig,
|
||||||
|
ReloadProfiles,
|
||||||
|
SelectProfile(Option<String>),
|
||||||
|
CreateProfile(String, ProfileBase),
|
||||||
|
DeleteProfile(String),
|
||||||
AskConfirmation(ConfirmationOptions, Box<AppMsg>),
|
AskConfirmation(ConfirmationOptions, Box<AppMsg>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,10 +10,10 @@ args = ["clap"]
|
|||||||
amdgpu-sysfs = { workspace = true }
|
amdgpu-sysfs = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_with = { workspace = true }
|
serde_with = { workspace = true }
|
||||||
serde-error = "=0.1.2"
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
indexmap = { workspace = true }
|
||||||
|
|
||||||
indexmap = { version = "*", features = ["serde"] }
|
serde-error = "=0.1.2"
|
||||||
clap = { version = "4.4.18", features = ["derive"], optional = true }
|
clap = { version = "4.4.18", features = ["derive"], optional = true }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
@ -306,3 +306,9 @@ pub struct FanOptions<'a> {
|
|||||||
pub spindown_delay_ms: Option<u64>,
|
pub spindown_delay_ms: Option<u64>,
|
||||||
pub change_threshold: 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 crate::FanOptions;
|
||||||
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -57,6 +59,17 @@ pub enum Request<'a> {
|
|||||||
VbiosDump {
|
VbiosDump {
|
||||||
id: &'a str,
|
id: &'a str,
|
||||||
},
|
},
|
||||||
|
ListProfiles,
|
||||||
|
SetProfile {
|
||||||
|
name: Option<String>,
|
||||||
|
},
|
||||||
|
CreateProfile {
|
||||||
|
name: String,
|
||||||
|
base: ProfileBase,
|
||||||
|
},
|
||||||
|
DeleteProfile {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
EnableOverdrive,
|
EnableOverdrive,
|
||||||
DisableOverdrive,
|
DisableOverdrive,
|
||||||
GenerateSnapshot,
|
GenerateSnapshot,
|
||||||
@ -83,3 +96,22 @@ pub enum SetClocksCommand {
|
|||||||
VoltageOffset(i32),
|
VoltageOffset(i32),
|
||||||
Reset,
|
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