feat(rog-control-center): Implement logic for fans, aura, and monitoring

This commit is contained in:
mihai2mn
2026-01-24 16:53:31 +01:00
parent 55b7c24556
commit 23207ab8ae
15 changed files with 1434 additions and 441 deletions

6
diagnose.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/bash
git status > status.txt 2>&1
echo "--- LOG ---" >> status.txt
git log -1 >> status.txt 2>&1
echo "--- REBASE DIR ---" >> status.txt
ls -d .git/rebase* >> status.txt 2>&1

View File

@@ -29,6 +29,7 @@ rog_dbus = { path = "../rog-dbus" }
rog_aura = { path = "../rog-aura" }
rog_profiles = { path = "../rog-profiles" }
rog_platform = { path = "../rog-platform" }
rog_slash = { path = "../rog-slash" }
supergfxctl = { git = "https://gitlab.com/asus-linux/supergfxctl.git", default-features = false }
dmi_id = { path = "../dmi-id" }
@@ -39,12 +40,14 @@ env_logger.workspace = true
tokio.workspace = true
serde.workspace = true
zbus.workspace = true
ron.workspace = true
dirs.workspace = true
notify-rust.workspace = true
concat-idents.workspace = true
futures-util.workspace = true
versions.workspace = true
serde_json = "1.0.149"
[dependencies.slint]
git = "https://github.com/slint-ui/slint.git"

View File

@@ -11,16 +11,21 @@ use gumdrop::Options;
use log::{debug, info, warn, LevelFilter};
use rog_control_center::cli_options::CliStart;
use rog_control_center::config::Config;
use tokio::runtime::Runtime;
thread_local! {
pub static UI: std::cell::RefCell<Option<MainWindow>> = Default::default();
pub static TRAY_TOOLTIP: std::cell::RefCell<Option<TrayTooltip>> = Default::default();
}
use rog_control_center::error::Result;
use rog_control_center::notify::start_notifications;
use rog_control_center::slint::ComponentHandle;
use rog_control_center::tray::init_tray;
use rog_control_center::tray::{init_tray, TrayEvent, TrayStats};
use rog_control_center::ui::setup_window;
use rog_control_center::zbus_proxies::{
AppState, ROGCCZbus, ROGCCZbusProxyBlocking, ZBUS_IFACE, ZBUS_PATH,
};
use rog_control_center::{print_versions, MainWindow};
use tokio::runtime::Runtime;
use rog_control_center::{print_versions, MainWindow, TrayTooltip};
#[tokio::main]
async fn main() -> Result<()> {
@@ -165,17 +170,33 @@ async fn main() -> Result<()> {
start_notifications(config.clone(), &rt)?;
if enable_tray_icon {
init_tray(supported_properties, config.clone());
}
let (tray_tx, mut tray_rx) = tokio::sync::mpsc::unbounded_channel();
// Channel for broadcasting system stats to the tray tooltip
let (stats_tx, stats_rx) = tokio::sync::watch::channel(TrayStats::default());
thread_local! { pub static UI: std::cell::RefCell<Option<MainWindow>> = Default::default()};
if enable_tray_icon {
init_tray(supported_properties, config.clone(), tray_tx, stats_rx);
}
// i_slint_backend_selector::with_platform(|_| Ok(())).unwrap();
if !startup_in_background {
if let Ok(mut app_state) = app_state.lock() {
*app_state = AppState::MainWindowShouldOpen;
}
} else {
// Even in background, we need the UI handle for status polling and tray sync
let config_copy = config.clone();
let stats_tx_copy = stats_tx.clone();
slint::invoke_from_event_loop(move || {
UI.with(|ui_cell| {
let mut ui = ui_cell.borrow_mut();
if ui.is_none() {
let newui = setup_window(config_copy, stats_tx_copy.clone());
ui.replace(newui);
}
});
})
.ok();
}
if std::env::var("RUST_TRANSLATIONS").is_ok() {
@@ -187,18 +208,21 @@ async fn main() -> Result<()> {
slint::init_translations!(concat!(env!("CARGO_MANIFEST_DIR"), "/translations/"));
}
// Prefetch supported Aura modes once at startup and move into the
// spawned UI thread so the UI uses a stable, immutable list.
let prefetched_supported: std::sync::Arc<Option<Vec<i32>>> = std::sync::Arc::new(
rog_control_center::ui::setup_aura::prefetch_supported_basic_modes().await,
);
thread::spawn(move || {
let mut state = AppState::StartingUp;
loop {
// Handle tray events
while let Ok(event) = tray_rx.try_recv() {
match event {
TrayEvent::ToggleTooltip(_, _) => {
// Native tooltip handled by ksni, no custom window needed
}
}
}
if is_rog_ally {
let config_copy_2 = config.clone();
let newui = setup_window(config.clone(), prefetched_supported.clone());
let newui = setup_window(config.clone(), stats_tx.clone());
newui.window().on_close_requested(move || {
exit(0);
});
@@ -239,9 +263,7 @@ async fn main() -> Result<()> {
let config_copy = config.clone();
let app_state_copy = app_state.clone();
// Avoid moving the original `prefetched_supported` into the
// closure — clone an Arc for the closure to capture.
let pref_for_invoke = prefetched_supported.clone();
let stats_tx_loop = stats_tx.clone();
slint::invoke_from_event_loop(move || {
UI.with(|ui| {
let app_state_copy = app_state_copy.clone();
@@ -256,7 +278,7 @@ async fn main() -> Result<()> {
});
} else {
let config_copy_2 = config_copy.clone();
let newui = setup_window(config_copy, pref_for_invoke.clone());
let newui = setup_window(config_copy, stats_tx_loop.clone());
newui.window().on_close_requested(move || {
if let Ok(mut app_state) = app_state_copy.lock() {
*app_state = AppState::MainWindowClosed;

View File

@@ -8,7 +8,6 @@ use rog_platform::supported::{
PlatformProfileFunctions, RogBiosSupportedFunctions, SupportedFunctions,
};
use rog_profiles::fan_curve_set::{CurveData, FanCurveSet};
use supergfxctl::pci_device::{GfxMode, GfxPower};
use crate::error::Result;

View File

@@ -1,4 +1,4 @@
//! `update_and_notify` is responsible for both notifications *and* updating
//! update_and_notify is responsible for both notifications and updating
//! stored statuses about the system state. This is done through either direct,
//! intoify, zbus notifications or similar methods.
//!
@@ -11,9 +11,10 @@ use std::time::Duration;
use log::{debug, error, info, warn};
use notify_rust::{Hint, Notification, Timeout};
use rog_dbus::zbus_platform::PlatformProxy;
use rog_platform::platform::PlatformProfile;
use rog_platform::power::AsusPower;
use serde::{Deserialize, Serialize};
use supergfxctl::pci_device::GfxPower;
use tokio::runtime::Runtime;
use tokio::task::JoinHandle;
@@ -26,64 +27,67 @@ const NOTIF_HEADER: &str = "ROG Control";
#[serde(default)]
pub struct EnabledNotifications {
pub enabled: bool,
pub receive_notify_gfx: bool,
pub receive_notify_gfx_status: bool,
pub receive_notify_platform_profile: bool,
}
impl Default for EnabledNotifications {
fn default() -> Self {
Self {
enabled: true,
receive_notify_gfx: true,
receive_notify_gfx_status: true,
receive_notify_platform_profile: true,
}
}
}
fn start_dpu_status_mon(config: Arc<Mutex<Config>>) {
use supergfxctl::pci_device::Device;
let dev = Device::find().unwrap_or_default();
let mut found_dgpu = false; // just for logging
for dev in dev {
if dev.is_dgpu() {
info!(
"Found dGPU: {}, starting status notifications",
dev.pci_id()
);
let enabled_notifications_copy = config.clone();
// Plain old thread is perfectly fine since most of this is potentially blocking
std::thread::spawn(move || {
let mut last_status = GfxPower::Unknown;
loop {
std::thread::sleep(Duration::from_millis(1500));
if let Ok(status) = dev.get_runtime_status() {
if status != GfxPower::Unknown && status != last_status {
if let Ok(config) = enabled_notifications_copy.lock() {
if !config.notifications.receive_notify_gfx_status
|| !config.notifications.enabled
{
continue;
}
}
// Required check because status cycles through
// active/unknown/suspended
do_gpu_status_notif("dGPU status changed:", &status)
.show()
.unwrap()
.on_close(|_| ());
debug!("dGPU status changed: {:?}", &status);
}
last_status = status;
}
/// Start monitoring for platform profile changes (triggered by Fn+F5 or software)
/// and display an OSD notification when the profile changes.
fn start_platform_profile_mon(config: Arc<Mutex<Config>>, rt: &Runtime) {
let enabled_notifications_copy = config.clone();
rt.spawn(async move {
let conn = match zbus::Connection::system().await {
Ok(c) => c,
Err(e) => {
error!("zbus signal: platform_profile_mon: {e}");
return;
}
};
let proxy = match PlatformProxy::builder(&conn).build().await {
Ok(p) => p,
Err(e) => {
error!("zbus signal: platform_profile_mon proxy: {e}");
return;
}
};
// Get initial profile to avoid notification on startup
let mut last_profile = proxy.platform_profile().await.ok();
info!("Started platform profile change monitor");
use futures_util::StreamExt;
let mut stream = proxy.receive_platform_profile_changed().await;
while let Some(e) = stream.next().await {
if let Ok(config) = enabled_notifications_copy.lock() {
if !config.notifications.enabled
|| !config.notifications.receive_notify_platform_profile
{
continue;
}
});
found_dgpu = true;
break;
}
if let Ok(new_profile) = e.get().await {
// Only show notification if profile actually changed
if last_profile != Some(new_profile) {
debug!("Platform profile changed to: {:?}", new_profile);
if let Err(e) = do_platform_profile_notif("Power Profile:", &new_profile)
.show()
.map(|n| n.on_close(|_| ()))
{
warn!("Failed to show platform profile notification: {e}");
}
last_profile = Some(new_profile);
}
}
}
}
if !found_dgpu {
warn!("Did not find a dGPU on this system, dGPU status won't be avilable");
}
});
}
pub fn start_notifications(
@@ -141,45 +145,8 @@ pub fn start_notifications(
}
});
info!("Attempting to start plain dgpu status monitor");
start_dpu_status_mon(config.clone());
// GPU MUX Mode notif
// TODO: need to get armoury attrs and iter to find
// let enabled_notifications_copy = config.clone();
// tokio::spawn(async move {
// let conn = zbus::Connection::system().await.map_err(|e| {
// error!("zbus signal: receive_notify_gpu_mux_mode: {e}");
// e
// })?;
// let proxy = PlatformProxy::new(&conn).await.map_err(|e| {
// error!("zbus signal: receive_notify_gpu_mux_mode: {e}");
// e
// })?;
// let mut actual_mux_mode = GpuMode::Error;
// if let Ok(mode) = proxy.gpu_mux_mode().await {
// actual_mux_mode = GpuMode::from(mode);
// }
// info!("Started zbus signal thread: receive_notify_gpu_mux_mode");
// while let Some(e) =
// proxy.receive_gpu_mux_mode_changed().await.next().await { if let
// Ok(config) = enabled_notifications_copy.lock() { if
// !config.notifications.enabled || !config.notifications.receive_notify_gfx {
// continue;
// }
// }
// if let Ok(out) = e.get().await {
// let mode = GpuMode::from(out);
// if mode == actual_mux_mode {
// continue;
// }
// do_mux_notification("Reboot required. BIOS GPU MUX mode set to",
// &mode).ok(); }
// }
// Ok::<(), zbus::Error>(())
// });
info!("Starting platform profile change monitor");
start_platform_profile_mon(config.clone(), rt);
Ok(vec![blocking])
}
@@ -197,14 +164,26 @@ where
notif
}
fn do_gpu_status_notif(message: &str, data: &GfxPower) -> Notification {
let mut notif = base_notification(message, &<&str>::from(data).to_owned());
let icon = match data {
GfxPower::Suspended => "asus_notif_blue",
GfxPower::Off => "asus_notif_green",
GfxPower::AsusDisabled => "asus_notif_white",
GfxPower::AsusMuxDiscreet | GfxPower::Active => "asus_notif_red",
GfxPower::Unknown => "gpu-integrated",
/// Create a notification for platform profile (power mode) changes.
/// Uses profile-specific icons and user-friendly names.
fn do_platform_profile_notif(message: &str, profile: &PlatformProfile) -> Notification {
let profile_name = match profile {
PlatformProfile::Balanced => "Balanced",
PlatformProfile::Performance => "Performance",
PlatformProfile::Quiet => "Quiet",
PlatformProfile::LowPower => "Low Power",
PlatformProfile::Custom => "Custom",
};
let mut notif = base_notification(message, &profile_name.to_owned());
// Use appropriate icons for each profile
// These icons should be available in the system or ROG icon pack
let icon = match profile {
PlatformProfile::Balanced => "asus_notif_blue", // Blue for balanced
PlatformProfile::Performance => "asus_notif_red", // Red for performance
PlatformProfile::Quiet => "asus_notif_green", // Green for quiet/power saving
PlatformProfile::LowPower => "asus_notif_green", // Green for low power
PlatformProfile::Custom => "asus_notif_white", // White for custom
};
notif.icon(icon);
notif

View File

@@ -6,12 +6,9 @@ use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use ksni::{Handle, Icon, TrayMethods};
use log::{info, warn};
use ksni::{Icon, TrayMethods};
use log::info;
use rog_platform::platform::Properties;
use supergfxctl::pci_device::{Device, GfxMode, GfxPower};
use supergfxctl::zbus_proxy::DaemonProxy as GfxProxy;
use versions::Versioning;
use crate::config::Config;
use crate::zbus_proxies::{AppState, ROGCCZbusProxyBlocking};
@@ -20,11 +17,8 @@ const TRAY_LABEL: &str = "ROG Control Center";
const TRAY_ICON_PATH: &str = "/usr/share/icons/hicolor/512x512/apps/";
struct Icons {
rog_blue: Icon,
#[allow(dead_code)]
rog_red: Icon,
rog_green: Icon,
rog_white: Icon,
gpu_integrated: Icon,
}
static ICONS: OnceLock<Icons> = OnceLock::new();
@@ -59,6 +53,29 @@ struct AsusTray {
current_title: String,
current_icon: Icon,
proxy: ROGCCZbusProxyBlocking<'static>,
tray_channel: Option<tokio::sync::mpsc::UnboundedSender<TrayEvent>>,
// System stats for native tooltip
cpu_temp: String,
gpu_temp: String,
cpu_fan: String,
gpu_fan: String,
power_w: String,
power_profile: String,
}
#[derive(Debug, Clone, Copy)]
pub enum TrayEvent {
ToggleTooltip(i32, i32),
}
#[derive(Debug, Clone, Default)]
pub struct TrayStats {
pub cpu_temp: String,
pub gpu_temp: String,
pub cpu_fan: String,
pub gpu_fan: String,
pub power_w: String,
pub power_profile: String,
}
impl ksni::Tray for AsusTray {
@@ -78,6 +95,26 @@ impl ksni::Tray for AsusTray {
ksni::Status::Active
}
fn activate(&mut self, x: i32, y: i32) {
if let Some(tx) = &self.tray_channel {
let _ = tx.send(TrayEvent::ToggleTooltip(x, y));
}
}
fn tool_tip(&self) -> ksni::ToolTip {
ksni::ToolTip {
title: "ROG Control Center".into(),
description: format!(
"<b>Profile:</b> {}\n<b>CPU:</b> {}°C | <b>GPU:</b> {}°C\n<b>Fans:</b> {} / {} RPM\n<b>Power:</b> {} W",
self.power_profile,
self.cpu_temp, self.gpu_temp,
self.cpu_fan, self.gpu_fan,
self.power_w
),
..Default::default()
}
}
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
use ksni::menu::*;
vec![
@@ -102,60 +139,13 @@ impl ksni::Tray for AsusTray {
}
}
async fn set_tray_icon_and_tip(
mode: GfxMode,
power: GfxPower,
tray: &mut Handle<AsusTray>,
supergfx_active: bool,
) {
if let Some(icons) = ICONS.get() {
let icon = match power {
GfxPower::Suspended => icons.rog_blue.clone(),
GfxPower::Off => {
if mode == GfxMode::Vfio {
icons.rog_red.clone()
} else {
icons.rog_green.clone()
}
}
GfxPower::AsusDisabled => icons.rog_white.clone(),
GfxPower::AsusMuxDiscreet | GfxPower::Active => icons.rog_red.clone(),
GfxPower::Unknown => {
if supergfx_active {
icons.gpu_integrated.clone()
} else {
icons.rog_red.clone()
}
}
};
tray.update(|tray: &mut AsusTray| {
tray.current_icon = icon;
tray.current_title = format!(
"ROG: gpu mode = {mode:?}, gpu power =
{power:?}"
);
})
.await;
}
}
fn find_dgpu() -> Option<Device> {
use supergfxctl::pci_device::Device;
let dev = Device::find().unwrap_or_default();
for dev in dev {
if dev.is_dgpu() {
info!("Found dGPU: {}", dev.pci_id());
// Plain old thread is perfectly fine since most of this is potentially blocking
return Some(dev);
}
}
warn!("Did not find a dGPU on this system, dGPU status won't be avilable");
None
}
/// The tray is controlled somewhat by `Arc<Mutex<SystemState>>`
pub fn init_tray(_supported_properties: Vec<Properties>, config: Arc<Mutex<Config>>) {
pub fn init_tray(
_supported_properties: Vec<Properties>,
config: Arc<Mutex<Config>>,
tray_channel: tokio::sync::mpsc::UnboundedSender<TrayEvent>,
mut stats_rx: tokio::sync::watch::Receiver<TrayStats>,
) {
tokio::spawn(async move {
let user_con = zbus::blocking::Connection::session().unwrap();
let proxy = ROGCCZbusProxyBlocking::new(&user_con).unwrap();
@@ -166,10 +156,18 @@ pub fn init_tray(_supported_properties: Vec<Properties>, config: Arc<Mutex<Confi
current_title: TRAY_LABEL.to_string(),
current_icon: rog_red.clone(),
proxy,
tray_channel: Some(tray_channel),
// Initialize stats fields
cpu_temp: "--".into(),
gpu_temp: "--".into(),
cpu_fan: "--".into(),
gpu_fan: "--".into(),
power_w: "--".into(),
power_profile: "Unknown".into(),
};
// TODO: return an error to the UI
let mut tray;
let tray;
match tray_init.disable_dbus_name(true).spawn().await {
Ok(t) => tray = t,
Err(e) => {
@@ -181,72 +179,32 @@ pub fn init_tray(_supported_properties: Vec<Properties>, config: Arc<Mutex<Confi
}
info!("Tray started");
let rog_blue = read_icon(&PathBuf::from("asus_notif_blue.png"));
let rog_green = read_icon(&PathBuf::from("asus_notif_green.png"));
let rog_white = read_icon(&PathBuf::from("asus_notif_white.png"));
let gpu_integrated = read_icon(&PathBuf::from("rog-control-center.png"));
ICONS.get_or_init(|| Icons {
rog_blue,
rog_red: rog_red.clone(),
rog_green,
rog_white,
gpu_integrated,
});
let mut has_supergfx = false;
let conn = zbus::Connection::system().await.unwrap();
if let Ok(gfx_proxy) = GfxProxy::new(&conn).await {
match gfx_proxy.mode().await {
Ok(_) => {
has_supergfx = true;
if let Ok(version) = gfx_proxy.version().await {
if let Some(version) = Versioning::new(&version) {
let curr_gfx = Versioning::new("5.2.0").unwrap();
warn!("supergfxd version = {version}");
if version < curr_gfx {
// Don't allow mode changing if too old a version
warn!("supergfxd found but is too old to use");
has_supergfx = false;
}
}
}
}
Err(e) => match e {
zbus::Error::MethodError(_, _, message) => {
warn!(
"Couldn't get mode from supergfxd: {message:?}, the supergfxd service \
may not be running or installed"
)
}
_ => warn!("Couldn't get mode from supergfxd: {e:?}"),
},
}
info!("Started ROGTray");
info!("Started ROGTray");
let mut last_power = GfxPower::Unknown;
let dev = find_dgpu();
loop {
tokio::time::sleep(Duration::from_millis(1000)).await;
if let Ok(lock) = config.try_lock() {
if !lock.enable_tray_icon {
return;
}
loop {
tokio::select! {
_ = stats_rx.changed() => {
let stats = stats_rx.borrow().clone();
let tray_update = tray.clone();
tokio::spawn(async move {
tray_update.update(move |t| {
t.cpu_temp = stats.cpu_temp;
t.gpu_temp = stats.gpu_temp;
t.cpu_fan = stats.cpu_fan;
t.gpu_fan = stats.gpu_fan;
t.power_w = stats.power_w;
t.power_profile = stats.power_profile;
}).await;
});
}
if has_supergfx {
if let Ok(mode) = gfx_proxy.mode().await {
if let Ok(power) = gfx_proxy.power().await {
if last_power != power {
set_tray_icon_and_tip(mode, power, &mut tray, has_supergfx).await;
last_power = power;
}
}
}
} else if let Some(dev) = dev.as_ref() {
if let Ok(power) = dev.get_runtime_status() {
if last_power != power {
set_tray_icon_and_tip(GfxMode::Hybrid, power, &mut tray, has_supergfx)
.await;
last_power = power;
_ = tokio::time::sleep(Duration::from_millis(1000)) => {
if let Ok(lock) = config.try_lock() {
if !lock.enable_tray_icon {
return;
}
}
}

View File

@@ -1,6 +1,10 @@
pub mod setup_anime;
pub mod setup_aura;
pub mod setup_fan_curve_custom;
pub mod setup_fans;
pub mod setup_screenpad;
pub mod setup_slash;
pub mod setup_status;
pub mod setup_system;
use std::sync::{Arc, Mutex};
@@ -11,9 +15,13 @@ use rog_dbus::list_iface_blocking;
use slint::{ComponentHandle, SharedString, Weak};
use crate::config::Config;
use crate::tray::TrayStats;
use crate::ui::setup_anime::setup_anime_page;
use crate::ui::setup_aura::setup_aura_page;
use crate::ui::setup_fans::setup_fan_curve_page;
use crate::ui::setup_screenpad::setup_screenpad;
use crate::ui::setup_slash::setup_slash;
use crate::ui::setup_status::setup_status;
use crate::ui::setup_system::{setup_system_page, setup_system_page_callbacks};
use crate::{AppSettingsPageData, MainWindow};
@@ -84,7 +92,7 @@ pub fn show_toast(
pub fn setup_window(
config: Arc<Mutex<Config>>,
prefetched_supported: std::sync::Arc<Option<Vec<i32>>>,
stats_tx: tokio::sync::watch::Sender<TrayStats>,
) -> MainWindow {
slint::set_xdg_app_id("rog-control-center")
.map_err(|e| warn!("Couldn't set application ID: {e:?}"))
@@ -104,10 +112,14 @@ pub fn setup_window(
available.contains(&"xyz.ljones.Platform".to_string()),
available.contains(&"xyz.ljones.Aura".to_string()),
available.contains(&"xyz.ljones.Anime".to_string()),
available.contains(&"xyz.ljones.Slash".to_string()),
false,
// Screenpad check (Backlight interface)
available.contains(&"xyz.ljones.Backlight".to_string()),
available.contains(&"xyz.ljones.FanCurves".to_string()),
true, // GPU Configuration
true, // App Settings
true, // About
true,
true,
true,
]
.into(),
);
@@ -116,21 +128,60 @@ pub fn setup_window(
slint::quit_event_loop().unwrap();
});
// Auto-hide toast logic
let toast_gen = Arc::new(Mutex::new(0u64));
let ui_weak = ui.as_weak();
ui.on_start_toast_timer(move || {
let toast_gen_clone = toast_gen.clone();
let ui_weak_clone = ui_weak.clone();
let my_gen = {
if let Ok(mut g) = toast_gen.lock() {
*g += 1;
*g
} else {
0
}
};
if my_gen > 0 {
tokio::spawn(async move {
tokio::time::sleep(tokio::time::Duration::from_secs(4)).await;
if let Ok(g) = toast_gen_clone.lock() {
if *g == my_gen {
let _ =
ui_weak_clone.upgrade_in_event_loop(move |ui| ui.invoke_hide_toast());
}
}
});
}
});
setup_app_settings_page(&ui, config.clone());
if available.contains(&"xyz.ljones.Platform".to_string()) {
setup_system_page(&ui, config.clone());
setup_system_page_callbacks(&ui, config.clone());
}
if available.contains(&"xyz.ljones.Aura".to_string()) {
setup_aura_page(&ui, config.clone(), prefetched_supported.as_ref().clone());
setup_aura_page(&ui, config.clone());
}
if available.contains(&"xyz.ljones.Anime".to_string()) {
setup_anime_page(&ui, config.clone());
}
if available.contains(&"xyz.ljones.FanCurves".to_string()) {
setup_fan_curve_page(&ui, config);
if available.contains(&"xyz.ljones.Slash".to_string()) {
setup_slash(&ui, config.clone());
}
// We didn't capture the boolean above. Let's just run it, it handles its own availability check internally via async proxy creation.
if available.contains(&"xyz.ljones.Backlight".to_string()) {
setup_screenpad(&ui, config.clone());
}
if available.contains(&"xyz.ljones.FanCurves".to_string()) {
setup_fan_curve_page(&ui, config.clone());
}
setup_status(&ui, config, stats_tx);
ui
}
@@ -157,18 +208,31 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc<Mutex<Config>>) {
lock.write();
}
});
// Master notifications toggle
let config_copy = config.clone();
global.on_set_enable_dgpu_notifications(move |enable| {
global.on_set_notifications_enabled(move |enable| {
if let Ok(mut lock) = config_copy.try_lock() {
lock.notifications.enabled = enable;
lock.write();
}
});
// Granular notification toggles
let config_copy = config.clone();
global.on_set_notify_platform_profile(move |enable| {
if let Ok(mut lock) = config_copy.try_lock() {
lock.notifications.receive_notify_platform_profile = enable;
lock.write();
}
});
// Initialize UI values from config
if let Ok(lock) = config.try_lock() {
global.set_run_in_background(lock.run_in_background);
global.set_startup_in_background(lock.startup_in_background);
global.set_enable_tray_icon(lock.enable_tray_icon);
global.set_enable_dgpu_notifications(lock.notifications.enabled);
global.set_notifications_enabled(lock.notifications.enabled);
global.set_notify_platform_profile(lock.notifications.receive_notify_platform_profile);
}
}

View File

@@ -1,8 +1,9 @@
use std::sync::{Arc, Mutex};
use log::{debug, error, info};
use rog_aura::animation::AnimationMode;
use rog_aura::keyboard::LaptopAuraPower;
use rog_aura::{AuraDeviceType, PowerZones};
use rog_aura::{AuraDeviceType, Colour, PowerZones};
use rog_dbus::zbus_aura::AuraProxy;
use slint::{ComponentHandle, Model, RgbaColor, SharedString};
@@ -34,108 +35,63 @@ fn decode_hex(s: &str) -> RgbaColor<u8> {
}
}
/// Returns the first available Aura interface
// TODO: return all
async fn find_aura_iface() -> Result<AuraProxy<'static>, Box<dyn std::error::Error>> {
let conn = zbus::Connection::system().await?;
let mgr = zbus::fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/").await?;
let objs = mgr.get_managed_objects().await?;
let mut paths: Vec<zbus::zvariant::OwnedObjectPath> = objs
.iter()
.filter(|(_, ifaces)| ifaces.keys().any(|k| k.as_str() == "xyz.ljones.Aura"))
.map(|(p, _)| p.clone())
.collect();
if paths.len() > 1 {
log::debug!("Multiple aura devices: {paths:?}");
let f = zbus::fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/").await?;
let interfaces = f.get_managed_objects().await?;
let mut aura_paths = Vec::new();
for v in interfaces.iter() {
for k in v.1.keys() {
if k.as_str() == "xyz.ljones.Aura" {
println!("Found aura device at {}, {}", v.0, k);
aura_paths.push(v.0.clone());
}
}
}
let path = paths.pop().ok_or("No Aura interface")?;
AuraProxy::builder(&conn)
.path(path)?
.destination("xyz.ljones.Asusd")?
.build()
.await
.map_err(Into::into)
if aura_paths.len() > 1 {
println!("Multiple aura devices found: {aura_paths:?}");
println!("TODO: enable selection");
}
if let Some(path) = aura_paths.first() {
return Ok(AuraProxy::builder(&conn)
.path(path.clone())?
.destination("xyz.ljones.Asusd")?
.build()
.await?);
}
Err("No Aura interface".into())
}
pub async fn prefetch_supported_basic_modes() -> Option<Vec<i32>> {
let proxy = find_aura_iface().await.ok()?;
let modes = proxy.supported_basic_modes().await.ok()?;
Some(modes.iter().map(|n| (*n).into()).collect())
}
pub fn setup_aura_page(
ui: &MainWindow,
_states: Arc<Mutex<Config>>,
prefetched_supported: Option<Vec<i32>>,
) {
let g = ui.global::<AuraPageData>();
g.on_cb_hex_from_colour(|c| {
pub fn setup_aura_page(ui: &MainWindow, _states: Arc<Mutex<Config>>) {
ui.global::<AuraPageData>().on_cb_hex_from_colour(|c| {
format!("#{:02X}{:02X}{:02X}", c.red(), c.green(), c.blue()).into()
});
g.on_cb_hex_to_colour(|s| decode_hex(s.as_str()).into());
ui.global::<AuraPageData>()
.on_cb_hex_to_colour(|s| decode_hex(s.as_str()).into());
let handle = ui.as_weak();
tokio::spawn(async move {
let Ok(aura) = find_aura_iface().await else {
info!("No aura interfaces");
info!("This device appears to have no aura interfaces");
return Ok::<(), zbus::Error>(());
};
set_ui_props_async!(handle, aura, AuraPageData, brightness);
set_ui_props_async!(handle, aura, AuraPageData, led_mode);
set_ui_props_async!(handle, aura, AuraPageData, led_mode_data);
set_ui_props_async!(handle, aura, AuraPageData, led_power);
set_ui_props_async!(handle, aura, AuraPageData, device_type);
if let Ok(data) = aura.led_mode_data().await {
let d = data.into();
handle
.upgrade_in_event_loop(move |h| {
h.global::<AuraPageData>().invoke_update_led_mode_data(d);
})
.ok();
}
let modes_vec: Vec<i32> = match prefetched_supported {
Some(p) => p,
None => aura
.supported_basic_modes()
.await
.ok()
.map(|m| m.iter().map(|n| (*n).into()).collect())
.unwrap_or_default(),
};
let current_mode: Option<i32> = aura.led_mode().await.ok().map(|m| m.into());
handle
.upgrade_in_event_loop(move |handle| {
let names = handle.global::<AuraPageData>().get_mode_names();
let mut raws = Vec::new();
let mut mode_names = Vec::new();
for (i, name) in names.iter().enumerate() {
let raw = i as i32;
if modes_vec.contains(&raw) && i != 9 {
raws.push(raw);
mode_names.push(name.clone());
}
}
handle
.global::<AuraPageData>()
.set_supported_basic_modes(raws.as_slice().into());
handle
.global::<AuraPageData>()
.set_available_mode_names(mode_names.as_slice().into());
if let Some(cm) = current_mode {
let idx = raws.iter().position(|&r| r == cm).unwrap_or(0) as i32;
handle
.global::<AuraPageData>()
.set_current_available_mode(idx);
}
})
.map_err(|e| error!("{e}"))
.ok();
if let Ok(mut pow3r) = aura.supported_power_zones().await {
let dev = aura
let dev_type = aura
.device_type()
.await
.unwrap_or(AuraDeviceType::LaptopKeyboard2021);
log::debug!("Available LED power modes {pow3r:?}");
handle
.upgrade_in_event_loop(move |handle| {
let names: Vec<SharedString> = handle
@@ -143,103 +99,282 @@ pub fn setup_aura_page(
.get_power_zone_names()
.iter()
.collect();
if dev.is_old_laptop() {
if dev_type.is_old_laptop() {
// Need to add the specific KeyboardAndLightbar
if pow3r.contains(&PowerZones::Keyboard)
&& pow3r.contains(&PowerZones::Lightbar)
{
pow3r.push(PowerZones::KeyboardAndLightbar);
}
let n: Vec<SharedString> =
pow3r.iter().map(|z| names[(*z) as usize].clone()).collect();
let names: Vec<SharedString> =
pow3r.iter().map(|n| names[(*n) as usize].clone()).collect();
handle
.global::<AuraPageData>()
.set_power_zone_names_old(n.as_slice().into());
.set_power_zone_names_old(names.as_slice().into());
} else {
let p: Vec<SlintPowerZones> = pow3r.iter().map(|z| (*z).into()).collect();
let power: Vec<SlintPowerZones> =
pow3r.iter().map(|p| (*p).into()).collect();
handle
.global::<AuraPageData>()
.set_supported_power_zones(p.as_slice().into());
.set_supported_power_zones(power.as_slice().into());
}
})
.ok();
}
let proxy = aura.clone();
let weak = handle.clone();
if let Ok(modes) = aura.supported_basic_modes().await {
log::debug!("Available LED modes {modes:?}");
// Check if only Static mode is available (enable software animation)
let static_only = modes.len() == 1 && modes.iter().any(|m| *m == 0.into());
// Clone proxy for callbacks
let aura_for_animation = aura.clone();
handle
.upgrade_in_event_loop(move |handle| {
let m: Vec<i32> = modes.iter().map(|n| (*n).into()).collect();
handle
.global::<AuraPageData>()
.set_supported_basic_modes(m.as_slice().into());
// Get the translated names
let names = handle.global::<AuraPageData>().get_mode_names();
let res: Vec<SharedString> = names
.iter()
.enumerate()
.filter(|(n, _)| modes.contains(&(*n as i32).into()) && *n != 9)
.map(|(_, i)| i)
.collect();
handle
.global::<AuraPageData>()
.set_available_mode_names(res.as_slice().into());
// Enable software animation if only Static mode is available
if static_only {
info!("Only Static mode available - enabling software animation controls");
handle
.global::<AuraPageData>()
.set_soft_animation_available(true);
// Connect mode callback - uses DBus to start animation in daemon
let aura_mode = aura_for_animation.clone();
let handle_weak = handle.as_weak();
handle
.global::<AuraPageData>()
.on_cb_soft_animation_mode(move |mode| {
let aura_inner = aura_mode.clone();
let handle = match handle_weak.upgrade() {
Some(h) => h,
None => return,
};
let data = handle.global::<AuraPageData>().get_led_mode_data();
let c1 = data.colour1;
let c2 = data.colour2;
let c1_rog = Colour {
r: c1.red(),
g: c1.green(),
b: c1.blue(),
};
let c2_rog = Colour {
r: c2.red(),
g: c2.green(),
b: c2.blue(),
};
let anim_mode = match mode {
1 => AnimationMode::Rainbow { speed_ms: 100 },
2 => AnimationMode::ColorCycle {
speed_ms: 200,
colors: vec![
Colour { r: 255, g: 0, b: 0 },
Colour { r: 0, g: 255, b: 0 },
Colour { r: 0, g: 0, b: 255 },
],
},
3 => AnimationMode::Breathe {
speed_ms: 100,
color1: c1_rog,
color2: c2_rog,
},
4 => AnimationMode::Pulse {
speed_ms: 50,
color: c1_rog,
min_brightness: 0.2,
max_brightness: 1.0,
},
_ => AnimationMode::None,
};
tokio::spawn(async move {
let json =
serde_json::to_string(&anim_mode).unwrap_or_default();
if anim_mode == AnimationMode::None {
if let Err(e) = aura_inner.stop_animation().await {
error!("Failed to stop animation: {e}");
}
} else {
if let Err(e) = aura_inner.start_animation(json).await {
error!("Failed to start animation: {e}");
}
}
});
});
}
})
.map_err(|e| error!("{e:}"))
.ok();
}
let proxy_copy = aura.clone();
handle
.upgrade_in_event_loop(move |h| {
set_ui_callbacks!(h,
.upgrade_in_event_loop(move |handle| {
set_ui_callbacks!(handle,
AuraPageData(.into()),
proxy.brightness(.into()),
"Brightness set to {}",
"Brightness failed"
proxy_copy.brightness(.into()),
"Keyboard LED brightness successfully set to {}",
"Setting keyboard LED brightness failed"
);
let p = proxy.clone();
let w = weak.clone();
h.global::<AuraPageData>().on_apply_led_mode_data(move || {
let Some(ui) = w.upgrade() else { return };
let slint_effect = ui.global::<AuraPageData>().get_led_mode_data();
let raw: rog_aura::AuraEffect = slint_effect.into();
let pp = p.clone();
let t = w.clone();
tokio::spawn(async move {
let r = pp.set_led_mode_data(raw).await;
show_toast("LED mode applied".into(), "LED mode failed".into(), t, r);
set_ui_callbacks!(handle,
AuraPageData(.into()),
proxy_copy.led_mode(.into()),
"Keyboard LED mode successfully set to {}",
"Setting keyboard LEDmode failed"
);
let proxy_data = proxy_copy.clone();
let aura_soft = proxy_copy.clone();
let handle_weak = handle.as_weak();
handle
.global::<AuraPageData>()
.on_cb_led_mode_data(move |data| {
// 1. Update hardware mode
let p = proxy_data.clone();
let d = data.clone();
tokio::spawn(async move {
if let Err(e) = p.set_led_mode_data(d.into()).await {
error!("Setting keyboard LED mode failed: {e}");
} else {
debug!("Keyboard LED mode set");
}
});
// 2. Update software animation if active
let handle = match handle_weak.upgrade() {
Some(h) => h,
None => return,
};
let soft_mode = handle.global::<AuraPageData>().get_soft_animation_mode();
if soft_mode != 0 {
let c1 = data.colour1;
let c2 = data.colour2;
let c1_rog = Colour {
r: c1.red(),
g: c1.green(),
b: c1.blue(),
};
let c2_rog = Colour {
r: c2.red(),
g: c2.green(),
b: c2.blue(),
};
let anim_mode = match soft_mode {
1 => AnimationMode::Rainbow { speed_ms: 100 },
2 => AnimationMode::ColorCycle {
speed_ms: 200,
colors: vec![
Colour { r: 255, g: 0, b: 0 },
Colour { r: 0, g: 255, b: 0 },
Colour { r: 0, g: 0, b: 255 },
],
},
3 => AnimationMode::Breathe {
speed_ms: 100,
color1: c1_rog,
color2: c2_rog,
},
4 => AnimationMode::Pulse {
speed_ms: 50,
color: c1_rog,
min_brightness: 0.2,
max_brightness: 1.0,
},
_ => AnimationMode::None,
};
let aura_s = aura_soft.clone();
tokio::spawn(async move {
if let Ok(json) = serde_json::to_string(&anim_mode) {
if let Err(e) = aura_s.start_animation(json).await {
error!("Failed to update software animation: {e}");
}
}
});
}
});
});
h.invoke_external_colour_change();
// set_ui_callbacks!(handle,
// AuraPageData(.clone().into()),
// proxy_copy.led_power(.into()),
// "Keyboard LED power successfully set to {:?}",
// "Setting keyboard power failed"
// );
handle.invoke_external_colour_change();
})
.ok();
let weak_power = handle.clone();
let proxy_power = aura.clone();
let handle_copy = handle.clone();
let proxy_copy = aura.clone();
handle
.upgrade_in_event_loop(|h| {
h.global::<AuraPageData>().on_cb_led_power(move |power| {
let w = weak_power.clone();
let p = proxy_power.clone();
let pw: LaptopAuraPower = power.into();
tokio::spawn(async move {
show_toast(
"Aura power updated".into(),
"Aura power failed".into(),
w,
p.set_led_power(pw).await,
);
.upgrade_in_event_loop(|handle| {
handle
.global::<AuraPageData>()
.on_cb_led_power(move |power| {
let handle_copy = handle_copy.clone();
let proxy_copy = aura.clone();
let power: LaptopAuraPower = power.into();
tokio::spawn(async move {
show_toast(
"Aura power settings changed".into(),
"Failed to set Aura power settings".into(),
handle_copy,
proxy_copy.set_led_power(power).await,
);
});
});
});
})
.map_err(|e| error!("{e}"))
.map_err(|e| error!("{e:}"))
.ok();
let stream_handle = handle.clone();
let aura_stream = aura.clone();
// Need to update the UI if the mode changes
let handle_copy = handle.clone();
// spawn required since the while let never exits
tokio::spawn(async move {
let mut x = proxy_copy.receive_led_mode_data_changed().await;
use futures_util::StreamExt;
let mut stream = aura_stream.receive_led_mode_data_changed().await;
while let Some(e) = stream.next().await {
while let Some(e) = x.next().await {
if let Ok(out) = e.get().await {
let raw: i32 = out.mode.into();
let data = out.into();
stream_handle
.upgrade_in_event_loop(move |h| {
h.global::<AuraPageData>().invoke_update_led_mode_data(data);
let supported: Vec<i32> = h
handle_copy
.upgrade_in_event_loop(move |handle| {
handle
.global::<AuraPageData>()
.get_supported_basic_modes()
.iter()
.collect();
let idx = supported.iter().position(|&x| x == raw).unwrap_or(0) as i32;
h.global::<AuraPageData>().set_current_available_mode(idx);
h.invoke_external_colour_change();
.invoke_update_led_mode_data(out.into());
handle.invoke_external_colour_change();
})
.map_err(|e| error!("{e}"))
.map_err(|e| error!("{e:}"))
.ok();
}
}
});
debug!("Aura setup done");
debug!("Aura setup tasks complete");
Ok(())
});
}

View File

@@ -0,0 +1,241 @@
use std::fs;
use std::path::{Path, PathBuf};
use log::{error, info};
use serde::{Deserialize, Serialize};
use crate::{FanType, MainWindow, Node};
const ASUS_CUSTOM_FAN_NAME: &str = "asus_custom_fan_curve";
const CONFIG_FILE_NAME: &str = "custom_fans.ron";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomCurvePoint {
pub temp: u8,
pub pwm: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CustomFanConfig {
pub cpu_curve: Vec<CustomCurvePoint>,
pub gpu_curve: Vec<CustomCurvePoint>,
pub enabled: bool,
}
#[derive(Clone)]
struct SysfsPaths {
root: PathBuf,
}
impl SysfsPaths {
fn new() -> Option<Self> {
let hwmon = Path::new("/sys/class/hwmon");
if let Ok(entries) = fs::read_dir(hwmon) {
for entry in entries.flatten() {
let path = entry.path();
let name_path = path.join("name");
if let Ok(name) = fs::read_to_string(&name_path) {
if name.trim() == ASUS_CUSTOM_FAN_NAME {
info!("Found ASUS Custom Fan Control at {:?}", path);
return Some(Self { root: path });
}
}
}
}
None
}
fn enable_path(&self, index: u8) -> PathBuf {
self.root.join(format!("pwm{}_enable", index))
}
fn point_pwm_path(&self, fan_idx: u8, point_idx: u8) -> PathBuf {
self.root
.join(format!("pwm{}_auto_point{}_pwm", fan_idx, point_idx))
}
fn point_temp_path(&self, fan_idx: u8, point_idx: u8) -> PathBuf {
self.root
.join(format!("pwm{}_auto_point{}_temp", fan_idx, point_idx))
}
}
// Helper to write with logging
fn write_sysfs(path: &Path, value: &str) -> std::io::Result<()> {
// debug!("Writing {} to {:?}", value, path);
fs::write(path, value)
}
fn _read_sysfs_u8(path: &Path) -> std::io::Result<u8> {
let s = fs::read_to_string(path)?;
s.trim()
.parse::<u8>()
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
}
pub fn is_custom_fan_supported() -> bool {
SysfsPaths::new().is_some()
}
// Logic to apply a full curve to a specific fan (1=CPU, 2=GPU usually)
// Implements the "Gradual Descent" algorithm
fn apply_curve_to_fan(
paths: &SysfsPaths,
fan_idx: u8,
points: &[CustomCurvePoint],
) -> std::io::Result<()> {
// Sort target points by temp (Hardware Requirement)
let mut sorted_target = points.to_vec();
sorted_target.sort_by_key(|p| p.temp);
// Ensure we have 8 points (fill with last if needed, or sensible default)
while sorted_target.len() < 8 {
if let Some(last) = sorted_target.last() {
sorted_target.push(last.clone());
} else {
sorted_target.push(CustomCurvePoint {
temp: 100,
pwm: 255,
});
}
}
sorted_target.truncate(8);
// Validate Temp Order (Synchronous Check)
for (i, p) in sorted_target.iter().enumerate() {
if i > 0 {
let prev_temp = sorted_target[i - 1].temp;
if p.temp < prev_temp {
error!("Invalid temp order");
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Temp disorder",
));
}
}
}
// Spawn completely detached thread for ALL I/O
let paths_clone = paths.clone();
let sorted_target = sorted_target.clone();
std::thread::spawn(move || {
let paths = paths_clone;
// 1. Enable custom mode
if let Err(e) = write_sysfs(&paths.enable_path(fan_idx), "1") {
error!("Failed to enable custom fan mode: {}", e);
return;
}
// 2. Write Temps
for (i, p) in sorted_target.iter().enumerate() {
let point_idx = (i + 1) as u8;
if let Err(e) = write_sysfs(
&paths.point_temp_path(fan_idx, point_idx),
&p.temp.to_string(),
) {
error!("Failed to write temp point {}: {}", point_idx, e);
}
}
// 3. Write PWMs directly (hardware handles gradual transition)
for (i, target_p) in sorted_target.iter().enumerate() {
let point_idx = (i + 1) as u8;
if let Err(e) = write_sysfs(
&paths.point_pwm_path(fan_idx, point_idx),
&target_p.pwm.to_string(),
) {
error!("Failed to write PWM point {}: {}", point_idx, e);
}
}
// 4. Ensure enable is set
let _ = write_sysfs(&paths.enable_path(fan_idx), "1");
});
Ok(())
}
fn set_fan_auto(paths: &SysfsPaths, fan_idx: u8) -> std::io::Result<()> {
// 2 = Auto (usually)
write_sysfs(&paths.enable_path(fan_idx), "2")
}
fn load_config() -> CustomFanConfig {
if let Some(config_dir) = dirs::config_dir() {
let path = config_dir.join("rog").join(CONFIG_FILE_NAME);
if let Ok(content) = fs::read_to_string(path) {
if let Ok(cfg) = ron::from_str(&content) {
return cfg;
}
}
}
CustomFanConfig::default()
}
fn save_config(config: &CustomFanConfig) {
if let Some(config_dir) = dirs::config_dir() {
let rog_dir = config_dir.join("rog");
let _ = fs::create_dir_all(&rog_dir);
let path = rog_dir.join(CONFIG_FILE_NAME);
if let Ok(s) = ron::ser::to_string_pretty(config, ron::ser::PrettyConfig::default()) {
let _ = fs::write(path, s);
}
}
}
// Public entry point called from setup_fans.rs or similar
// Returns immediately - all work is done in a detached thread
pub fn apply_custom_fan_curve(
_handle_weak: slint::Weak<MainWindow>,
fan_type: FanType,
enabled: bool,
nodes: Vec<Node>,
) {
// Fan Index: 1=CPU, 2=GPU usually.
let fan_idx = match fan_type {
FanType::CPU => 1,
FanType::GPU => 2,
_ => return, // Ignore others
};
// Convert nodes to points (fast, CPU-only)
let points: Vec<CustomCurvePoint> = nodes
.iter()
.map(|n| CustomCurvePoint {
temp: n.x as u8,
pwm: n.y as u8,
})
.collect();
// Spawn a completely detached thread for ALL I/O
std::thread::spawn(move || {
// Get paths (blocking FS operation)
let Some(paths) = SysfsPaths::new() else {
error!("No custom fan support found");
return;
};
// Save config
let mut cfg = load_config();
if enabled {
match fan_type {
FanType::CPU => cfg.cpu_curve = points.clone(),
FanType::GPU => cfg.gpu_curve = points.clone(),
_ => {}
}
}
cfg.enabled = enabled;
save_config(&cfg);
// Apply curve or set auto
if enabled {
if let Err(e) = apply_curve_to_fan(&paths, fan_idx, &points) {
error!("Failed to apply fan curve: {}", e);
}
} else if let Err(e) = set_fan_auto(&paths, fan_idx) {
error!("Failed to set fan auto: {}", e);
}
});
}

View File

@@ -1,4 +1,6 @@
use std::sync::{Arc, Mutex};
use crate::ui::show_toast;
use std::collections::HashMap;
use std::sync::{Arc, Mutex, OnceLock};
use log::error;
use rog_dbus::zbus_fan_curves::FanCurvesProxy;
@@ -8,7 +10,25 @@ use rog_profiles::fan_curve_set::CurveData;
use slint::{ComponentHandle, Model, Weak};
use crate::config::Config;
use crate::{FanPageData, FanType, MainWindow, Node};
use crate::{FanPageData, FanType, MainWindow, Node, Profile};
// Isolated Rust-side cache for fan curves (not affected by Slint reactivity)
type FanCacheKey = (i32, i32); // (Profile as i32, FanType as i32)
static FAN_CACHE: OnceLock<Mutex<HashMap<FanCacheKey, Vec<Node>>>> = OnceLock::new();
fn fan_cache() -> &'static Mutex<HashMap<FanCacheKey, Vec<Node>>> {
FAN_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
}
fn cache_fan_curve(profile: Profile, fan_type: FanType, nodes: Vec<Node>) {
let key = (profile as i32, fan_type as i32);
fan_cache().lock().unwrap().insert(key, nodes);
}
fn get_cached_fan_curve(profile: Profile, fan_type: FanType) -> Option<Vec<Node>> {
let key = (profile as i32, fan_type as i32);
fan_cache().lock().unwrap().get(&key).cloned()
}
pub fn update_fan_data(
handle: Weak<MainWindow>,
@@ -19,7 +39,7 @@ pub fn update_fan_data(
handle
.upgrade_in_event_loop(move |handle| {
let global = handle.global::<FanPageData>();
let collect = |temp: &[u8], pwm: &[u8]| -> slint::ModelRc<Node> {
let _collect = |temp: &[u8], pwm: &[u8]| -> slint::ModelRc<Node> {
let tmp: Vec<Node> = temp
.iter()
.zip(pwm.iter())
@@ -33,61 +53,100 @@ pub fn update_fan_data(
for fan in bal {
global.set_balanced_available(true);
let nodes_vec: Vec<Node> = fan
.temp
.iter()
.zip(fan.pwm.iter())
.map(|(x, y)| Node {
x: *x as f32,
y: *y as f32,
})
.collect();
let nodes: slint::ModelRc<Node> = nodes_vec.as_slice().into();
match fan.fan {
rog_profiles::FanCurvePU::CPU => {
global.set_cpu_fan_available(true);
global.set_balanced_cpu_enabled(fan.enabled);
global.set_balanced_cpu(collect(&fan.temp, &fan.pwm))
global.set_balanced_cpu(nodes.clone());
cache_fan_curve(Profile::Balanced, FanType::CPU, nodes_vec);
}
rog_profiles::FanCurvePU::GPU => {
global.set_gpu_fan_available(true);
global.set_balanced_gpu_enabled(fan.enabled);
global.set_balanced_gpu(collect(&fan.temp, &fan.pwm))
global.set_balanced_gpu(nodes.clone());
cache_fan_curve(Profile::Balanced, FanType::GPU, nodes_vec);
}
rog_profiles::FanCurvePU::MID => {
global.set_mid_fan_available(true);
global.set_balanced_mid_enabled(fan.enabled);
global.set_balanced_mid(collect(&fan.temp, &fan.pwm))
global.set_balanced_mid(nodes.clone());
cache_fan_curve(Profile::Balanced, FanType::Middle, nodes_vec);
}
}
}
for fan in perf {
global.set_performance_available(true);
let nodes_vec: Vec<Node> = fan
.temp
.iter()
.zip(fan.pwm.iter())
.map(|(x, y)| Node {
x: *x as f32,
y: *y as f32,
})
.collect();
let nodes: slint::ModelRc<Node> = nodes_vec.as_slice().into();
match fan.fan {
rog_profiles::FanCurvePU::CPU => {
global.set_cpu_fan_available(true);
global.set_performance_cpu_enabled(fan.enabled);
global.set_performance_cpu(collect(&fan.temp, &fan.pwm))
global.set_performance_cpu(nodes.clone());
cache_fan_curve(Profile::Performance, FanType::CPU, nodes_vec);
}
rog_profiles::FanCurvePU::GPU => {
global.set_gpu_fan_available(true);
global.set_performance_gpu_enabled(fan.enabled);
global.set_performance_gpu(collect(&fan.temp, &fan.pwm))
global.set_performance_gpu(nodes.clone());
cache_fan_curve(Profile::Performance, FanType::GPU, nodes_vec);
}
rog_profiles::FanCurvePU::MID => {
global.set_mid_fan_available(true);
global.set_performance_mid_enabled(fan.enabled);
global.set_performance_mid(collect(&fan.temp, &fan.pwm))
global.set_performance_mid(nodes.clone());
cache_fan_curve(Profile::Performance, FanType::Middle, nodes_vec);
}
}
}
for fan in quiet {
global.set_quiet_available(true);
let nodes_vec: Vec<Node> = fan
.temp
.iter()
.zip(fan.pwm.iter())
.map(|(x, y)| Node {
x: *x as f32,
y: *y as f32,
})
.collect();
let nodes: slint::ModelRc<Node> = nodes_vec.as_slice().into();
match fan.fan {
rog_profiles::FanCurvePU::CPU => {
global.set_cpu_fan_available(true);
global.set_quiet_cpu_enabled(fan.enabled);
global.set_quiet_cpu(collect(&fan.temp, &fan.pwm))
global.set_quiet_cpu(nodes.clone());
cache_fan_curve(Profile::Quiet, FanType::CPU, nodes_vec);
}
rog_profiles::FanCurvePU::GPU => {
global.set_gpu_fan_available(true);
global.set_quiet_gpu_enabled(fan.enabled);
global.set_quiet_gpu(collect(&fan.temp, &fan.pwm))
global.set_quiet_gpu(nodes.clone());
cache_fan_curve(Profile::Quiet, FanType::GPU, nodes_vec);
}
rog_profiles::FanCurvePU::MID => {
global.set_mid_fan_available(true);
global.set_quiet_mid_enabled(fan.enabled);
global.set_quiet_mid(collect(&fan.temp, &fan.pwm))
global.set_quiet_mid(nodes.clone());
cache_fan_curve(Profile::Quiet, FanType::Middle, nodes_vec);
}
}
}
@@ -171,6 +230,7 @@ pub fn setup_fan_curve_page(ui: &MainWindow, _config: Arc<Mutex<Config>>) {
let choices_for_ui = platform_profile_choices.clone();
let handle_next1 = handle_copy.clone();
if let Err(e) = handle_copy.upgrade_in_event_loop(move |handle| {
let handle_weak_for_fans = handle.as_weak();
let global = handle.global::<FanPageData>();
let fans1 = fans.clone();
let choices = choices_for_ui.clone();
@@ -212,17 +272,103 @@ pub fn setup_fan_curve_page(ui: &MainWindow, _config: Arc<Mutex<Config>>) {
update_fan_data(handle_next, balanced, perf, quiet);
});
});
let handle_weak_for_cancel = handle_weak_for_fans.clone();
global.on_set_fan_data(move |fan, profile, enabled, data| {
if crate::ui::setup_fan_curve_custom::is_custom_fan_supported() {
let handle_weak = handle_weak_for_fans.clone();
let data: Vec<Node> = data.iter().collect();
use log::info;
info!("MainThread: Request to apply custom curve for {:?}", fan);
// Explicitly spawn a thread to handle this, preventing ANY main thread blocking
std::thread::spawn(move || {
info!("WorkerThread: applying curve for {:?}", fan);
crate::ui::setup_fan_curve_custom::apply_custom_fan_curve(
handle_weak.clone(),
fan,
enabled,
data,
);
info!("WorkerThread: returned from apply (async), clearing busy flag for {:?}", fan);
// Clear busy flag
let _ = handle_weak.upgrade_in_event_loop(move |h| {
let g = h.global::<FanPageData>();
match fan {
FanType::CPU => g.set_is_busy_cpu(false),
FanType::GPU => g.set_is_busy_gpu(false),
FanType::Middle => g.set_is_busy_mid(false),
}
info!("MainThread: cleared busy flag for {:?}", fan);
});
});
return;
}
let fans = fans.clone();
let data: Vec<Node> = data.iter().collect();
let data = fan_data_for(fan, enabled, data);
let handle_weak = handle_weak_for_fans.clone();
let nodes_vec: Vec<Node> = data.iter().collect();
let _data_copy = nodes_vec.clone();
let cache_copy = nodes_vec.clone(); // Clone for cache update
let fan_data = fan_data_for(fan, enabled, nodes_vec);
tokio::spawn(async move {
fans.set_fan_curve(profile.into(), data)
.await
.map_err(|e| error!("{e:}"))
.ok()
show_toast(
"Fan curve applied".into(),
"Failed to apply fan curve".into(),
handle_weak.clone(),
fans.set_fan_curve(profile.into(), fan_data).await,
);
let _ = handle_weak.upgrade_in_event_loop(move |h| {
let g = h.global::<FanPageData>();
// Update Rust-side cache (isolated from Slint properties)
cache_fan_curve(profile, fan, cache_copy);
match fan {
FanType::CPU => g.set_is_busy_cpu(false),
FanType::GPU => g.set_is_busy_gpu(false),
FanType::Middle => g.set_is_busy_mid(false),
}
});
});
});
global.on_cancel(move |fan_type, profile| {
let handle_weak = handle_weak_for_cancel.clone();
let _ = handle_weak.upgrade_in_event_loop(move |h: MainWindow| {
let global = h.global::<FanPageData>();
// Retrieve from isolated Rust cache
let nodes_opt = get_cached_fan_curve(profile, fan_type);
if let Some(nodes_vec) = nodes_opt {
use log::info;
info!("Canceling {:?} {:?} - restoring {} nodes from isolated cache", fan_type, profile, nodes_vec.len());
let new_model: slint::ModelRc<Node> = nodes_vec.as_slice().into();
match (profile, fan_type) {
(crate::Profile::Balanced, FanType::CPU) => global.set_balanced_cpu(new_model),
(crate::Profile::Balanced, FanType::GPU) => global.set_balanced_gpu(new_model),
(crate::Profile::Balanced, FanType::Middle) => global.set_balanced_mid(new_model),
(crate::Profile::Performance, FanType::CPU) => global.set_performance_cpu(new_model),
(crate::Profile::Performance, FanType::GPU) => global.set_performance_gpu(new_model),
(crate::Profile::Performance, FanType::Middle) => global.set_performance_mid(new_model),
(crate::Profile::Quiet, FanType::CPU) => global.set_quiet_cpu(new_model),
(crate::Profile::Quiet, FanType::GPU) => global.set_quiet_gpu(new_model),
(crate::Profile::Quiet, FanType::Middle) => global.set_quiet_mid(new_model),
_ => {}
}
} else {
log::warn!("Cancel failed: No cached data for {:?} {:?}", fan_type, profile);
}
});
});
// Initialize warning
if crate::ui::setup_fan_curve_custom::is_custom_fan_supported() {
global.set_show_custom_warning(true);
}
}) {
error!("setup_fan_curve_page: upgrade_in_event_loop: {e:?}");
}

View File

@@ -0,0 +1,143 @@
use log::{debug, error};
use rog_dbus::zbus_backlight::BacklightProxy;
use slint::ComponentHandle;
use std::sync::{Arc, Mutex};
use crate::config::Config;
use crate::ui::show_toast;
use crate::{MainWindow, ScreenpadPageData};
pub fn setup_screenpad(ui: &MainWindow, _config: Arc<Mutex<Config>>) {
let handle = ui.as_weak();
tokio::spawn(async move {
// Create the connections/proxies here
let conn = match zbus::Connection::system().await {
Ok(conn) => conn,
Err(e) => {
error!("Failed to connect to system bus for Screenpad: {e:}");
return;
}
};
let backlight = match BacklightProxy::builder(&conn).build().await {
Ok(backlight) => backlight,
Err(e) => {
error!("Failed to create backlight proxy for Screenpad: {e:}");
return;
}
};
// Initialize state
debug!("Initializing Screenpad page data");
// Use helper to set initial properties
if let Ok(val) = backlight.screenpad_brightness().await {
handle
.upgrade_in_event_loop(move |h| {
h.global::<ScreenpadPageData>().set_brightness(val);
// Assume power is on if brightness > 0
h.global::<ScreenpadPageData>().set_power(val > 0);
})
.ok();
}
if let Ok(gamma_str) = backlight.screenpad_gamma().await {
if let Ok(gamma) = gamma_str.parse::<f32>() {
handle
.upgrade_in_event_loop(move |h| {
h.global::<ScreenpadPageData>().set_gamma(gamma);
})
.ok();
}
}
if let Ok(sync) = backlight.screenpad_sync_with_primary().await {
handle
.upgrade_in_event_loop(move |h| {
h.global::<ScreenpadPageData>().set_sync_with_primary(sync);
})
.ok();
}
// Set up callbacks
let handle_copy = handle.clone();
let backlight_copy = backlight.clone();
handle
.upgrade_in_event_loop(move |h| {
let global = h.global::<ScreenpadPageData>();
// Brightness Callback
let hl = handle_copy.clone();
let bl = backlight_copy.clone();
global.on_cb_brightness(move |val| {
let bl = bl.clone();
let hl = hl.clone();
tokio::spawn(async move {
show_toast(
format!("Screenpad brightness set to {}", val).into(),
"Failed to set Screenpad brightness".into(),
hl,
bl.set_screenpad_brightness(val).await,
);
});
});
// Gamma Callback
let hl = handle_copy.clone();
let bl = backlight_copy.clone();
global.on_cb_gamma(move |val| {
let bl = bl.clone();
let hl = hl.clone();
tokio::spawn(async move {
show_toast(
format!("Screenpad gamma set to {:.2}", val).into(),
"Failed to set Screenpad gamma".into(),
hl,
bl.set_screenpad_gamma(&val.to_string()).await,
);
});
});
// Sync Callback
let hl = handle_copy.clone();
let bl = backlight_copy.clone();
global.on_cb_sync_with_primary(move |val| {
let bl = bl.clone();
let hl = hl.clone();
tokio::spawn(async move {
show_toast(
format!(
"Screenpad sync {}",
if val { "enabled" } else { "disabled" }
)
.into(),
"Failed to toggle Screenpad sync".into(),
hl,
bl.set_screenpad_sync_with_primary(val).await,
);
});
});
// Power Callback (Toggle brightness to 0/last or 100)
let hl = handle_copy.clone();
let bl = backlight_copy.clone();
global.on_cb_power(move |val| {
let bl = bl.clone();
let hl = hl.clone();
tokio::spawn(async move {
let target = if val { 100 } else { 0 };
let _ = bl.set_screenpad_brightness(target).await;
hl.upgrade_in_event_loop(move |h| {
h.global::<ScreenpadPageData>().set_brightness(target);
})
.ok();
});
});
})
.ok();
// Optional: Value watches for external changes
// (Similar to setup_system.rs if needed)
});
}

View File

@@ -0,0 +1,132 @@
use crate::config::Config;
use crate::set_ui_callbacks;
use crate::ui::show_toast;
use crate::{MainWindow, SlashPageData};
use rog_dbus::{find_iface_async, zbus_slash::SlashProxy};
use rog_slash::SlashMode;
use slint::{ComponentHandle, Model};
use std::str::FromStr;
use std::sync::{Arc, Mutex};
pub fn setup_slash(ui: &MainWindow, _config: Arc<Mutex<Config>>) {
let ui_weak = ui.as_weak();
tokio::spawn(async move {
// Find the Slash interface proxy
let proxies = match find_iface_async::<SlashProxy>("xyz.ljones.Slash").await {
Ok(p) => p,
Err(e) => {
log::warn!("Failed to find Slash interface: {}", e);
return;
}
};
let proxy = match proxies.first() {
Some(p) => p.clone(),
None => return,
};
// UI Callbacks (MUST be done on UI thread to access global state)
{
let proxy_copy = proxy.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
let proxy = proxy_copy.clone();
let ui_handle = ui;
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.enabled(), "Slash enabled {}", "Failed to enable slash");
// Fix: Cast f32 (UI) to u8 (DBus)
set_ui_callbacks!(ui_handle, SlashPageData(as f32), proxy.brightness(as u8), "Slash brightness set to {}", "Failed to set slash brightness");
set_ui_callbacks!(ui_handle, SlashPageData(as f32), proxy.interval(as u8), "Slash interval set to {}", "Failed to set slash interval");
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_battery_warning(), "Battery warning set to {}", "Failed to set battery warning");
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_battery(), "Show on battery set to {}", "Failed to set show on battery");
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_boot(), "Show on boot set to {}", "Failed to set show on boot");
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_shutdown(), "Show on shutdown set to {}", "Failed to set show on shutdown");
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_sleep(), "Show on sleep set to {}", "Failed to set show on sleep");
set_ui_callbacks!(ui_handle, SlashPageData(), proxy.show_on_lid_closed(), "Show on lid closed set to {}", "Failed to set show on lid closed");
});
}
// Custom Mode Logic - Callback setup
{
let proxy_copy = proxy.clone();
let ui_weak_copy = ui_weak.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
let data = ui.global::<SlashPageData>();
data.on_cb_mode_index(move |idx| {
let proxy_copy = proxy_copy.clone();
let handle_weak = ui_weak_copy.clone();
let mode_str_opt = if let Some(h) = handle_weak.upgrade() {
let d = h.global::<SlashPageData>();
if idx >= 0 && (idx as usize) < d.get_modes().row_count() {
Some(d.get_modes().row_data(idx as usize).unwrap_or_default())
} else {
None
}
} else {
None
};
if let Some(mode_str) = mode_str_opt {
if let Ok(mode) = SlashMode::from_str(&mode_str) {
tokio::spawn(async move {
show_toast(
format!("Slash mode set to {}", mode).into(),
"Failed to set slash mode".into(),
handle_weak,
proxy_copy.set_mode(mode).await,
);
});
}
}
});
});
}
// D-Bus Signal -> UI
let proxy_copy = proxy.clone();
let handle_copy = ui_weak.clone();
tokio::spawn(async move {
let mut changes = proxy_copy.receive_mode_changed().await;
use futures_util::StreamExt;
while let Some(change) = changes.next().await {
if let Ok(mode) = change.get().await {
let mode_str = mode.to_string();
let handle_copy = handle_copy.clone();
let _ = slint::invoke_from_event_loop(move || {
if let Some(h) = handle_copy.upgrade() {
let d = h.global::<SlashPageData>();
let model = d.get_modes();
for (i, m) in model.iter().enumerate() {
if m == mode_str {
d.set_mode_index(i as i32);
break;
}
}
}
});
}
}
});
if let Ok(m) = proxy.mode().await {
let mode_str = m.to_string();
let _ = slint::invoke_from_event_loop(move || {
if let Some(h) = ui_weak.upgrade() {
let d = h.global::<SlashPageData>();
let model = d.get_modes();
for (i, m) in model.iter().enumerate() {
if m == mode_str {
d.set_mode_index(i as i32);
break;
}
}
}
});
}
});
}

View File

@@ -0,0 +1,150 @@
use crate::config::Config;
use crate::tray::TrayStats;
use crate::{MainWindow, SystemStatus};
use rog_dbus::zbus_platform::PlatformProxy;
use slint::ComponentHandle;
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tokio::fs;
use tokio::time::Duration;
use zbus::Connection;
pub fn setup_status(
ui: &MainWindow,
_config: Arc<Mutex<Config>>,
stats_tx: tokio::sync::watch::Sender<TrayStats>,
) {
let ui_weak = ui.as_weak();
tokio::spawn(async move {
let mut power_history: VecDeque<i32> = VecDeque::with_capacity(150); // 300s window at 2s poll
// DBus connection for profile
let conn = Connection::system().await.ok();
let platform = if let Some(c) = &conn {
PlatformProxy::new(c).await.ok()
} else {
None
};
loop {
let (cpu_temp, gpu_temp, cpu_fan, gpu_fan) = read_hwmon().await;
let power_microwatts = read_power().await;
// Rolling average logic
if power_history.len() >= 150 {
power_history.pop_front();
}
power_history.push_back(power_microwatts);
let sum: i64 = power_history.iter().map(|&x| x as i64).sum();
let avg_microwatts = if !power_history.is_empty() {
sum / power_history.len() as i64
} else {
0
};
// Convert to Watts
let power_w = power_microwatts as f64 / 1_000_000.0;
let avg_w = avg_microwatts as f64 / 1_000_000.0;
// Fetch profile
let mut profile_str = "Unknown".to_string();
if let Some(p) = &platform {
if let Ok(prof) = p.platform_profile().await {
profile_str = format!("{:?}", prof);
}
}
let ui_weak_loop = ui_weak.clone(); // Clone ui_weak for this iteration
// Send to Tray
let _ = stats_tx.send(TrayStats {
cpu_temp: format!("{}", cpu_temp),
gpu_temp: format!("{}", gpu_temp),
cpu_fan: format!("{}", cpu_fan),
gpu_fan: format!("{}", gpu_fan),
power_w: format!("{:.1}", power_w),
power_profile: profile_str,
});
let _ = slint::invoke_from_event_loop(move || {
if let Some(ui) = ui_weak_loop.upgrade() {
let global = ui.global::<SystemStatus>();
global.set_cpu_temp(cpu_temp);
global.set_gpu_temp(gpu_temp);
global.set_cpu_fan(cpu_fan);
global.set_gpu_fan(gpu_fan);
global.set_power_w(slint::SharedString::from(format!("{:.1}", power_w)));
global.set_power_avg_w(slint::SharedString::from(format!("{:.1}", avg_w)));
}
});
tokio::time::sleep(Duration::from_secs(2)).await;
}
});
}
async fn read_hwmon() -> (i32, i32, i32, i32) {
let mut cpu_temp = 0;
let mut gpu_temp = 0;
let mut cpu_fan = 0;
let mut gpu_fan = 0;
let mut entries = match fs::read_dir("/sys/class/hwmon").await {
Ok(e) => e,
Err(_) => return (0, 0, 0, 0),
};
while let Ok(Some(entry)) = entries.next_entry().await {
let path = entry.path();
let name_path = path.join("name");
if let Ok(name_str) = fs::read_to_string(&name_path).await {
let name = name_str.trim();
if name == "k10temp" || name == "coretemp" || name == "zenpower" {
// Try temp1_input (TCtl/Package)
if let Ok(temp) = read_val(&path.join("temp1_input")).await {
cpu_temp = temp / 1000;
}
} else if name == "amdgpu" || name == "nvidia" {
if let Ok(temp) = read_val(&path.join("temp1_input")).await {
gpu_temp = temp / 1000;
}
} else if name == "asus" || name == "asus_custom_fan_curve" {
if let Ok(fan) = read_val(&path.join("fan1_input")).await {
cpu_fan = fan;
}
if let Ok(fan) = read_val(&path.join("fan2_input")).await {
gpu_fan = fan;
}
}
}
}
(cpu_temp, gpu_temp, cpu_fan, gpu_fan)
}
async fn read_val(path: &PathBuf) -> Result<i32, ()> {
let s = fs::read_to_string(path).await.map_err(|_| ())?;
s.trim().parse::<i32>().map_err(|_| ())
}
async fn read_power() -> i32 {
let mut p = 0;
// Try BAT0 then BAT1
if let Ok(v) = read_val(&PathBuf::from("/sys/class/power_supply/BAT0/power_now")).await {
p = v.abs();
} else if let Ok(v) = read_val(&PathBuf::from("/sys/class/power_supply/BAT1/power_now")).await {
p = v.abs();
}
// Check status
if let Ok(s) = fs::read_to_string("/sys/class/power_supply/BAT0/status").await {
if s.trim() == "Discharging" {
return -p;
}
}
p
}

View File

@@ -47,7 +47,6 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc<Mutex<Config>>) {
ui.global::<SystemPageData>().set_screen_auto_brightness(-1);
ui.global::<SystemPageData>().set_mcu_powersave(-1);
ui.global::<SystemPageData>().set_mini_led_mode(-1);
ui.global::<SystemPageData>().set_screenpad_brightness(-1);
ui.global::<SystemPageData>().set_ppt_pl1_spl(MINMAX);
ui.global::<SystemPageData>().set_ppt_pl2_sppt(MINMAX);
ui.global::<SystemPageData>().set_ppt_pl3_fppt(MINMAX);
@@ -296,7 +295,7 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc<Mutex<Config>>)
log::error!("Failed to create platform proxy: {}", e);
})
.unwrap();
let backlight = BacklightProxy::builder(&conn)
let _backlight = BacklightProxy::builder(&conn)
.build()
.await
.map_err(|e| {
@@ -397,23 +396,7 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc<Mutex<Config>>)
set_ui_props_async!(handle, platform, SystemPageData, enable_ppt_group);
set_ui_props_async!(handle, backlight, SystemPageData, screenpad_brightness);
if let Ok(value) = backlight.screenpad_gamma().await {
handle
.upgrade_in_event_loop(move |handle| {
handle
.global::<SystemPageData>()
.set_screenpad_gamma(value.parse().unwrap_or(1.0));
})
.ok();
}
set_ui_props_async!(
handle,
backlight,
SystemPageData,
screenpad_sync_with_primary
);
set_ui_props_async!(handle, platform, SystemPageData, enable_ppt_group);
let platform_copy = platform.clone();
handle
@@ -536,25 +519,11 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc<Mutex<Config>>)
"Setting Throttle policy on AC failed"
);
set_ui_callbacks!(handle,
SystemPageData(as i32),
backlight.screenpad_brightness(as i32),
"Screenpad successfully set to {}",
"Setting screenpad brightness failed"
);
set_ui_callbacks!(handle,
SystemPageData(as bool),
backlight.screenpad_sync_with_primary(as bool),
"Screenpad successfully set to {}",
"Setting screenpad brightness failed"
);
set_ui_callbacks!(handle,
SystemPageData(.parse().unwrap_or(1.0)),
backlight.screenpad_gamma(.to_string().as_str()),
"Screenpad successfully set to {}",
"Setting screenpad brightness failed"
platform_copy.change_platform_profile_on_battery(.into()),
"Throttle policy on battery enabled: {}",
"Setting Throttle policy on AC failed"
);
})
.ok();

46
status.txt Normal file
View File

@@ -0,0 +1,46 @@
interactive rebase in progress; onto 5d4b164b
Last commands done (5 commands done):
pick 19dc1b28 # feat(asusd): Implement threaded Aura animator and hardware coordination
pick d92478e8 # feat(rog-control-center): Implement logic for fans, aura, and monitoring
(see more in file .git/rebase-merge/done)
Next command to do (1 remaining command):
pick 43227544 # chore: update dependencies and config
(use "git rebase --edit-todo" to view and edit)
You are currently rebasing branch 'main' on '5d4b164b'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: rog-control-center/Cargo.toml
modified: rog-control-center/src/mocking.rs
modified: rog-control-center/src/notify.rs
modified: rog-control-center/src/tray.rs
new file: rog-control-center/src/ui/setup_fan_curve_custom.rs
modified: rog-control-center/src/ui/setup_fans.rs
new file: rog-control-center/src/ui/setup_screenpad.rs
new file: rog-control-center/src/ui/setup_slash.rs
new file: rog-control-center/src/ui/setup_status.rs
modified: rog-control-center/src/ui/setup_system.rs
Unmerged paths:
(use "git restore --staged <file>..." to unstage)
(use "git add <file>..." to mark resolution)
both modified: rog-control-center/src/main.rs
both modified: rog-control-center/src/ui/mod.rs
both modified: rog-control-center/src/ui/setup_aura.rs
Untracked files:
(use "git add <file>..." to include in what will be committed)
diagnose.sh
status.txt
--- LOG ---
commit 55b7c245568e9eef869a3b69a57ef28b90000ab0
Author: mihai2mn <mihai2mn@users.noreply.gitlab.com>
Date: Sat Jan 24 16:53:08 2026 +0100
feat(asusd): Implement threaded Aura animator and hardware coordination
--- REBASE DIR ---
.git/rebase-merge