mirror of
https://github.com/ilya-zlobintsev/LACT.git
synced 2026-08-19 01:14:43 -05:00
feat: support exporting and importing profiles (#579)
* feat: support exporting and importing profiles * feat: renaming profiles
This commit is contained in:
Generated
+1
@@ -1382,6 +1382,7 @@ dependencies = [
|
||||
"relm4",
|
||||
"relm4-components",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thread-priority",
|
||||
"tracing",
|
||||
|
||||
@@ -3,7 +3,10 @@ mod connection;
|
||||
mod macros;
|
||||
|
||||
pub use lact_schema as schema;
|
||||
use lact_schema::{config::GpuConfig, ProfileRule};
|
||||
use lact_schema::{
|
||||
config::{GpuConfig, Profile},
|
||||
ProfileRule,
|
||||
};
|
||||
|
||||
use amdgpu_sysfs::gpu_handle::power_profile_mode::PowerProfileModesTable;
|
||||
use anyhow::Context;
|
||||
@@ -143,6 +146,10 @@ impl DaemonClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_profile(&self, name: Option<String>) -> anyhow::Result<Option<Profile>> {
|
||||
self.make_request(Request::GetProfile { name }).await
|
||||
}
|
||||
|
||||
pub async fn set_profile(&self, name: Option<String>, auto_switch: bool) -> anyhow::Result<()> {
|
||||
self.make_request(Request::SetProfile { name, auto_switch })
|
||||
.await
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::server::gpu_controller::{GpuController, VENDOR_NVIDIA};
|
||||
use anyhow::Context;
|
||||
use indexmap::IndexMap;
|
||||
use lact_schema::{config::GpuConfig, ProfileRule};
|
||||
use lact_schema::config::{GpuConfig, Profile};
|
||||
use nix::unistd::{getuid, Group};
|
||||
use notify::{RecommendedWatcher, Watcher};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -87,14 +87,6 @@ impl Default for Daemon {
|
||||
}
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct Profile {
|
||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub gpus: IndexMap<String, GpuConfig>,
|
||||
pub rule: Option<ProfileRule>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> anyhow::Result<Option<Self>> {
|
||||
let path = get_path(FILE_NAME);
|
||||
|
||||
@@ -171,6 +171,9 @@ async fn handle_request<'a>(request: Request<'a>, handler: &'a Handler) -> anyho
|
||||
Request::ListProfiles { include_state } => {
|
||||
ok_response(handler.list_profiles(include_state).await)
|
||||
}
|
||||
Request::GetProfile { name } => {
|
||||
ok_response(handler.get_profile(name.map(Into::into)).await?)
|
||||
}
|
||||
Request::SetProfile { name, auto_switch } => ok_response(
|
||||
handler
|
||||
.set_profile(name.map(Into::into), auto_switch)
|
||||
|
||||
@@ -5,7 +5,7 @@ use super::{
|
||||
};
|
||||
use crate::{
|
||||
bindings::intel::IntelDrm,
|
||||
config::{Config, Profile},
|
||||
config::Config,
|
||||
server::{gpu_controller::init_controller, profiles, system::DAEMON_VERSION},
|
||||
};
|
||||
use amdgpu_sysfs::gpu_handle::{
|
||||
@@ -13,7 +13,7 @@ use amdgpu_sysfs::gpu_handle::{
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context};
|
||||
use lact_schema::{
|
||||
config::{default_fan_static_speed, FanControlSettings, FanCurve, GpuConfig},
|
||||
config::{default_fan_static_speed, FanControlSettings, FanCurve, GpuConfig, Profile},
|
||||
default_fan_curve,
|
||||
request::{ClockspeedType, ConfirmCommand, ProfileBase, SetClocksCommand},
|
||||
ClocksInfo, DeviceInfo, DeviceListEntry, DeviceStats, FanControlMode, FanOptions, PmfwOptions,
|
||||
@@ -817,6 +817,16 @@ impl<'a> Handler {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_profile(&self, name: Option<Rc<str>>) -> anyhow::Result<Option<Profile>> {
|
||||
let config = self.config.read().await;
|
||||
|
||||
let profile = match name {
|
||||
Some(profile) => config.profiles.get(&profile).cloned(),
|
||||
None => Some(config.default_profile()),
|
||||
};
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
pub async fn set_profile(
|
||||
&self,
|
||||
name: Option<Rc<str>>,
|
||||
@@ -860,6 +870,7 @@ impl<'a> Handler {
|
||||
ProfileBase::Empty => Profile::default(),
|
||||
ProfileBase::Default => config.default_profile(),
|
||||
ProfileBase::Profile(name) => config.profile(&name)?.clone(),
|
||||
ProfileBase::Provided(profile) => profile,
|
||||
};
|
||||
config.profiles.insert(name.into(), profile);
|
||||
config.save(&self.config_last_saved)?;
|
||||
|
||||
@@ -22,6 +22,7 @@ tracing-subscriber = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
gtk = { version = "0.9", package = "gtk4", features = ["v4_6"] }
|
||||
|
||||
|
||||
+134
-3
@@ -29,8 +29,8 @@ use header::{
|
||||
use lact_client::{ConnectionStatusMsg, DaemonClient};
|
||||
use lact_schema::{
|
||||
args::GuiArgs,
|
||||
config::GpuConfig,
|
||||
request::{ConfirmCommand, SetClocksCommand},
|
||||
config::{GpuConfig, Profile},
|
||||
request::{ConfirmCommand, ProfileBase, SetClocksCommand},
|
||||
DeviceStats, GIT_COMMIT,
|
||||
};
|
||||
use msg::AppMsg;
|
||||
@@ -47,8 +47,14 @@ use relm4::{
|
||||
prelude::{AsyncComponent, AsyncComponentParts},
|
||||
tokio, AsyncComponentSender, Component, ComponentController, MessageBroker, RelmObjectExt,
|
||||
};
|
||||
use relm4_components::{
|
||||
open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogResponse, OpenDialogSettings},
|
||||
save_dialog::{SaveDialog, SaveDialogMsg, SaveDialogResponse, SaveDialogSettings},
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
os::unix::net::UnixStream,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU32, Ordering},
|
||||
@@ -80,13 +86,19 @@ pub struct AppModel {
|
||||
stats_task_handle: Option<glib::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CommandOutput {
|
||||
ProfileImport(PathBuf),
|
||||
Error(anyhow::Error),
|
||||
}
|
||||
|
||||
#[relm4::component(pub, async)]
|
||||
impl AsyncComponent for AppModel {
|
||||
type Init = GuiArgs;
|
||||
|
||||
type Input = AppMsg;
|
||||
type Output = ();
|
||||
type CommandOutput = ();
|
||||
type CommandOutput = Option<CommandOutput>;
|
||||
|
||||
view! {
|
||||
#[root]
|
||||
@@ -259,6 +271,19 @@ impl AsyncComponent for AppModel {
|
||||
}
|
||||
self.update_view(widgets, sender);
|
||||
}
|
||||
|
||||
async fn update_cmd(
|
||||
&mut self,
|
||||
msg: Self::CommandOutput,
|
||||
sender: AsyncComponentSender<Self>,
|
||||
_root: &Self::Root,
|
||||
) {
|
||||
if let Some(msg) = msg {
|
||||
if let Err(err) = self.handle_cmd_output(msg, &sender).await {
|
||||
sender.input(AppMsg::Error(Arc::new(err)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppModel {
|
||||
@@ -314,6 +339,28 @@ impl AppModel {
|
||||
include_state: false,
|
||||
});
|
||||
}
|
||||
AppMsg::RenameProfile(old_name, new_name) => {
|
||||
if old_name != new_name {
|
||||
let original_profile = self
|
||||
.daemon_client
|
||||
.get_profile(Some(old_name.clone()))
|
||||
.await
|
||||
.context("Could not get profile by old name")?
|
||||
.context("Original profile not found")?;
|
||||
self.daemon_client
|
||||
.create_profile(new_name, ProfileBase::Provided(original_profile))
|
||||
.await
|
||||
.context("Could not create new profile")?;
|
||||
self.daemon_client
|
||||
.delete_profile(old_name)
|
||||
.await
|
||||
.context("Could not delete old name")?;
|
||||
|
||||
sender.input(AppMsg::ReloadProfiles {
|
||||
include_state: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
AppMsg::DeleteProfile(profile) => {
|
||||
self.daemon_client.delete_profile(profile).await?;
|
||||
sender.input(AppMsg::ReloadProfiles {
|
||||
@@ -326,6 +373,56 @@ impl AppModel {
|
||||
include_state: false,
|
||||
});
|
||||
}
|
||||
AppMsg::ImportProfile => {
|
||||
let json_filter = gtk::FileFilter::new();
|
||||
json_filter.add_mime_type("application/json");
|
||||
|
||||
let settings = OpenDialogSettings {
|
||||
filters: vec![json_filter],
|
||||
..Default::default()
|
||||
};
|
||||
let file_picker = OpenDialog::builder().launch(settings);
|
||||
file_picker.emit(OpenDialogMsg::Open);
|
||||
let stream = file_picker.into_stream();
|
||||
|
||||
sender.oneshot_command(async move {
|
||||
if let Some(OpenDialogResponse::Accept(path)) = stream.recv_one().await {
|
||||
Some(CommandOutput::ProfileImport(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
}
|
||||
AppMsg::ExportProfile(name) => {
|
||||
if let Some(profile) = self.daemon_client.get_profile(name.clone()).await? {
|
||||
let settings = SaveDialogSettings {
|
||||
create_folders: true,
|
||||
is_modal: true,
|
||||
..Default::default()
|
||||
};
|
||||
let diag = SaveDialog::builder().launch(settings);
|
||||
diag.emit(SaveDialogMsg::SaveAs(format!(
|
||||
"LACT-profile-{}.json",
|
||||
name.as_deref().unwrap_or("default")
|
||||
)));
|
||||
|
||||
let stream = diag.into_stream();
|
||||
|
||||
sender.oneshot_command(async move {
|
||||
if let Some(SaveDialogResponse::Accept(path)) = stream.recv_one().await {
|
||||
let contents = serde_json::to_string(&profile)
|
||||
.expect("Could not serialize profile");
|
||||
|
||||
if let Err(err) =
|
||||
fs::write(path, contents).context("Could not export profile")
|
||||
{
|
||||
return Some(CommandOutput::Error(err));
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
}
|
||||
}
|
||||
AppMsg::Stats(stats) => {
|
||||
let update = PageUpdate::Stats(stats.clone());
|
||||
self.oc_page.emit(OcPageMsg::Update {
|
||||
@@ -423,6 +520,40 @@ impl AppModel {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_cmd_output(
|
||||
&mut self,
|
||||
msg: CommandOutput,
|
||||
sender: &AsyncComponentSender<AppModel>,
|
||||
) -> anyhow::Result<()> {
|
||||
match msg {
|
||||
CommandOutput::ProfileImport(path) => {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("Imported profile");
|
||||
|
||||
let contents = fs::read_to_string(&path).context("Could not read selected file")?;
|
||||
let profile = serde_json::from_str::<Profile>(&contents)
|
||||
.context("Could not parse profile")?;
|
||||
let profile_name = file_name
|
||||
.trim_start_matches("LACT-profile-")
|
||||
.trim_end_matches(".json");
|
||||
|
||||
self.daemon_client
|
||||
.create_profile(profile_name.to_owned(), ProfileBase::Provided(profile))
|
||||
.await
|
||||
.context("Could not import profile")?;
|
||||
|
||||
sender.input(AppMsg::ReloadProfiles {
|
||||
include_state: false,
|
||||
});
|
||||
}
|
||||
CommandOutput::Error(error) => return Err(error),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn current_gpu_id(&self) -> anyhow::Result<String> {
|
||||
self.header
|
||||
.model()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
mod new_profile_dialog;
|
||||
mod profile_rename_dialog;
|
||||
mod profile_row;
|
||||
pub mod profile_rule_window;
|
||||
|
||||
@@ -11,6 +12,7 @@ use gtk::*;
|
||||
use lact_client::schema::DeviceListEntry;
|
||||
use lact_schema::ProfilesInfo;
|
||||
use new_profile_dialog::NewProfileDialog;
|
||||
use profile_rename_dialog::ProfileRenameDialog;
|
||||
use profile_row::{ProfileRow, ProfileRowType};
|
||||
use profile_rule_window::{ProfileRuleWindow, ProfileRuleWindowMsg};
|
||||
use relm4::{
|
||||
@@ -37,9 +39,12 @@ pub enum HeaderMsg {
|
||||
Profiles(std::boxed::Box<ProfilesInfo>),
|
||||
AutoProfileSwitch(bool),
|
||||
ShowProfileEditor(DynamicIndex),
|
||||
ExportProfile(DynamicIndex),
|
||||
RenameProfile(DynamicIndex),
|
||||
SelectProfile,
|
||||
SelectGpu,
|
||||
CreateProfile,
|
||||
ImportProfile,
|
||||
ClosePopover,
|
||||
}
|
||||
|
||||
@@ -120,8 +125,16 @@ impl Component for Header {
|
||||
gtk::Button {
|
||||
set_expand: true,
|
||||
set_icon_name: "list-add",
|
||||
set_tooltip: "Add new profile",
|
||||
connect_clicked => HeaderMsg::CreateProfile,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_icon_name: "document-import-symbolic",
|
||||
set_tooltip: "Import profile from file",
|
||||
set_expand: true,
|
||||
connect_clicked => HeaderMsg::ImportProfile,
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -282,6 +295,20 @@ impl Component for Header {
|
||||
}
|
||||
}
|
||||
}
|
||||
HeaderMsg::ExportProfile(index) => {
|
||||
sender.input(HeaderMsg::ClosePopover);
|
||||
|
||||
let profile = self
|
||||
.profile_selector
|
||||
.get(index.current_index())
|
||||
.expect("No profile with given index");
|
||||
|
||||
let name = match &profile.row {
|
||||
ProfileRowType::Default => None,
|
||||
ProfileRowType::Profile { name, .. } => Some(name.clone()),
|
||||
};
|
||||
sender.output(AppMsg::ExportProfile(name)).unwrap();
|
||||
}
|
||||
HeaderMsg::CreateProfile => {
|
||||
sender.input(HeaderMsg::ClosePopover);
|
||||
|
||||
@@ -292,6 +319,33 @@ impl Component for Header {
|
||||
});
|
||||
diag_controller.detach_runtime();
|
||||
}
|
||||
HeaderMsg::RenameProfile(index) => {
|
||||
sender.input(HeaderMsg::ClosePopover);
|
||||
|
||||
let profile = self
|
||||
.profile_selector
|
||||
.get(index.current_index())
|
||||
.expect("No profile with given index");
|
||||
|
||||
let sender = sender.clone();
|
||||
if let ProfileRowType::Profile { name, .. } = profile.row.clone() {
|
||||
let stream = ProfileRenameDialog::builder()
|
||||
.launch(name.clone())
|
||||
.into_stream();
|
||||
|
||||
sender.clone().oneshot_command(async move {
|
||||
if let Some(new_name) = stream.recv_one().await {
|
||||
sender
|
||||
.output(AppMsg::RenameProfile(name, new_name))
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
HeaderMsg::ImportProfile => {
|
||||
sender.input(HeaderMsg::ClosePopover);
|
||||
sender.output(AppMsg::ImportProfile).unwrap();
|
||||
}
|
||||
HeaderMsg::ShowProfileEditor(index) => {
|
||||
sender.input(HeaderMsg::ClosePopover);
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
use gtk::prelude::{
|
||||
BoxExt, DialogExt, DialogExtManual, EditableExt, EntryExt, GtkWindowExt, OrientableExt,
|
||||
WidgetExt,
|
||||
};
|
||||
use relm4::{ComponentParts, ComponentSender, RelmWidgetExt};
|
||||
|
||||
pub struct ProfileRenameDialog {}
|
||||
|
||||
#[relm4::component(pub)]
|
||||
impl relm4::SimpleComponent for ProfileRenameDialog {
|
||||
type Init = String;
|
||||
type Input = ();
|
||||
type Output = String;
|
||||
|
||||
view! {
|
||||
gtk::Dialog {
|
||||
set_default_size: (400, 50),
|
||||
set_title: Some("Rename profile"),
|
||||
set_hide_on_close: true,
|
||||
connect_response[root, sender, name_entry] => move |_, response| {
|
||||
match response {
|
||||
gtk::ResponseType::Accept => {
|
||||
sender.output(name_entry.text().to_string()).unwrap();
|
||||
root.close();
|
||||
}
|
||||
gtk::ResponseType::Cancel => root.close(),
|
||||
_ => (),
|
||||
}
|
||||
},
|
||||
add_buttons: &[("Cancel", gtk::ResponseType::Cancel), ("Save", gtk::ResponseType::Accept)],
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Horizontal,
|
||||
set_margin_all: 5,
|
||||
set_spacing: 5,
|
||||
|
||||
gtk::Label {
|
||||
set_markup: &format!("Rename profile <b>{old_name}</b> to:"),
|
||||
},
|
||||
|
||||
#[name = "name_entry"]
|
||||
gtk::Entry {
|
||||
set_text: &old_name,
|
||||
set_hexpand: true,
|
||||
connect_activate[root] => move |_| {
|
||||
root.response(gtk::ResponseType::Accept);
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
fn init(
|
||||
old_name: Self::Init,
|
||||
root: Self::Root,
|
||||
sender: ComponentSender<Self>,
|
||||
) -> ComponentParts<Self> {
|
||||
let model = Self {};
|
||||
|
||||
let widgets = view_output!();
|
||||
|
||||
root.present();
|
||||
|
||||
ComponentParts { widgets, model }
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use crate::app::{msg::AppMsg, APP_BROKER};
|
||||
use gtk::{pango, prelude::*};
|
||||
use lact_schema::ProfileRule;
|
||||
use relm4::{
|
||||
css,
|
||||
factory::{DynamicIndex, FactoryComponent},
|
||||
FactorySender, RelmWidgetExt,
|
||||
};
|
||||
@@ -56,12 +57,53 @@ impl FactoryComponent for ProfileRow {
|
||||
set_width_request: 200,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_icon_name: "preferences-other-symbolic",
|
||||
set_tooltip: "Edit Profile Rules",
|
||||
set_sensitive: matches!(self.row, ProfileRowType::Profile { auto: true, .. }),
|
||||
connect_clicked[sender, index] => move |_| {
|
||||
sender.output(HeaderMsg::ShowProfileEditor(index.clone())).unwrap();
|
||||
gtk::MenuButton {
|
||||
set_icon_name: "open-menu-symbolic",
|
||||
#[wrap(Some)]
|
||||
set_popover = >k::Popover {
|
||||
set_margin_all: 5,
|
||||
|
||||
gtk::Box {
|
||||
set_orientation: gtk::Orientation::Vertical,
|
||||
set_spacing: 5,
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Rename Profile",
|
||||
set_sensitive: matches!(self.row, ProfileRowType::Profile { .. }),
|
||||
connect_clicked[sender, index] => move |_| {
|
||||
sender.output(HeaderMsg::RenameProfile(index.clone())).unwrap();
|
||||
},
|
||||
add_css_class: css::FLAT,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Delete Profile",
|
||||
set_sensitive: matches!(self.row, ProfileRowType::Profile { .. }),
|
||||
connect_clicked[profile = self.row.clone()] => move |_| {
|
||||
if let ProfileRowType::Profile { name, .. } = profile.clone() {
|
||||
APP_BROKER.send(AppMsg::DeleteProfile(name));
|
||||
}
|
||||
},
|
||||
add_css_class: css::FLAT,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Edit Activation Rules",
|
||||
set_sensitive: matches!(self.row, ProfileRowType::Profile { auto: true, .. }),
|
||||
connect_clicked[sender, index] => move |_| {
|
||||
sender.output(HeaderMsg::ShowProfileEditor(index.clone())).unwrap();
|
||||
},
|
||||
add_css_class: css::FLAT,
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_label: "Export To File",
|
||||
connect_clicked[sender, index] => move |_| {
|
||||
sender.output(HeaderMsg::ExportProfile(index.clone())).unwrap();
|
||||
},
|
||||
add_css_class: css::FLAT,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -90,17 +132,6 @@ impl FactoryComponent for ProfileRow {
|
||||
APP_BROKER.send(move_profile_msg(&profile, &index, 1));
|
||||
},
|
||||
},
|
||||
|
||||
gtk::Button {
|
||||
set_icon_name: "list-remove",
|
||||
set_sensitive: matches!(self.row, ProfileRowType::Profile { .. }),
|
||||
set_tooltip: "Delete Profile",
|
||||
connect_clicked[profile = self.row.clone()] => move |_| {
|
||||
if let ProfileRowType::Profile { name, .. } = profile.clone() {
|
||||
APP_BROKER.send(AppMsg::DeleteProfile(name));
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,14 @@ pub enum AppMsg {
|
||||
CreateProfile(String, ProfileBase),
|
||||
DeleteProfile(String),
|
||||
MoveProfile(String, usize),
|
||||
RenameProfile(String, String),
|
||||
EvaluateProfile(ProfileRule),
|
||||
SetProfileRule {
|
||||
name: String,
|
||||
rule: Option<ProfileRule>,
|
||||
},
|
||||
ImportProfile,
|
||||
ExportProfile(Option<String>),
|
||||
ConnectionStatus(ConnectionStatusMsg),
|
||||
AskConfirmation(ConfirmationOptions, Box<AppMsg>),
|
||||
}
|
||||
|
||||
@@ -6,9 +6,17 @@ use serde_with::skip_serializing_none;
|
||||
use crate::{
|
||||
default_fan_curve,
|
||||
request::{ClockspeedType, SetClocksCommand},
|
||||
FanControlMode, FanCurveMap, PmfwOptions,
|
||||
FanControlMode, FanCurveMap, PmfwOptions, ProfileRule,
|
||||
};
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct Profile {
|
||||
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
|
||||
pub gpus: IndexMap<String, GpuConfig>,
|
||||
pub rule: Option<ProfileRule>,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct GpuConfig {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{config::GpuConfig, FanOptions, ProfileRule};
|
||||
use crate::{
|
||||
config::{GpuConfig, Profile},
|
||||
FanOptions, ProfileRule,
|
||||
};
|
||||
use amdgpu_sysfs::gpu_handle::{PerformanceLevel, PowerLevelKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -63,6 +66,9 @@ pub enum Request<'a> {
|
||||
#[serde(default)]
|
||||
include_state: bool,
|
||||
},
|
||||
GetProfile {
|
||||
name: Option<String>,
|
||||
},
|
||||
SetProfile {
|
||||
name: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -143,6 +149,7 @@ pub enum ProfileBase {
|
||||
Empty,
|
||||
Default,
|
||||
Profile(String),
|
||||
Provided(Profile),
|
||||
}
|
||||
|
||||
impl fmt::Display for ProfileBase {
|
||||
@@ -151,6 +158,7 @@ impl fmt::Display for ProfileBase {
|
||||
ProfileBase::Empty => "Empty",
|
||||
ProfileBase::Default => "Default",
|
||||
ProfileBase::Profile(name) => name,
|
||||
ProfileBase::Provided(_) => "<Provided>",
|
||||
};
|
||||
text.fmt(f)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user