From e84ac9954b16e56ea3296004deae6919208fcb52 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:34:33 +0200 Subject: [PATCH 01/21] rog-platform: Add battery health, cycles, and power consumption helpers --- rog-platform/src/power.rs | 151 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/rog-platform/src/power.rs b/rog-platform/src/power.rs index 5d2d1c79..4af6cbc8 100644 --- a/rog-platform/src/power.rs +++ b/rog-platform/src/power.rs @@ -112,4 +112,155 @@ impl AsusPower { usb, }) } + + pub fn has_battery(&self) -> bool { + !self.battery.as_os_str().is_empty() + } + + pub fn get_battery_cycle_count(&self) -> Result { + let path = self.battery.join("cycle_count"); + if !path.exists() { + return Err(PlatformError::Read( + path.to_string_lossy().into(), + std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"), + )); + } + let content = std::fs::read_to_string(&path) + .map_err(|e| PlatformError::Read(path.to_string_lossy().into(), e))?; + content + .trim() + .parse::() + .map_err(|e| PlatformError::Read( + path.to_string_lossy().into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + )) + } + + pub fn get_battery_health(&self) -> Result { + let full = if self.battery.join("energy_full").exists() { + let content = std::fs::read_to_string(self.battery.join("energy_full")) + .map_err(|e| PlatformError::Read("energy_full".into(), e))?; + content.trim().parse::().ok() + } else if self.battery.join("charge_full").exists() { + let content = std::fs::read_to_string(self.battery.join("charge_full")) + .map_err(|e| PlatformError::Read("charge_full".into(), e))?; + content.trim().parse::().ok() + } else { + None + }; + + let design = if self.battery.join("energy_full_design").exists() { + let content = std::fs::read_to_string(self.battery.join("energy_full_design")) + .map_err(|e| PlatformError::Read("energy_full_design".into(), e))?; + content.trim().parse::().ok() + } else if self.battery.join("charge_full_design").exists() { + let content = std::fs::read_to_string(self.battery.join("charge_full_design")) + .map_err(|e| PlatformError::Read("charge_full_design".into(), e))?; + content.trim().parse::().ok() + } else { + None + }; + + match (full, design) { + (Some(f), Some(d)) if d > 0.0 => { + let health = (f / d * 100.0).round().min(100.0).max(0.0) as u8; + Ok(health) + } + _ => Err(PlatformError::Read( + self.battery.to_string_lossy().into(), + std::io::Error::new( + std::io::ErrorKind::NotFound, + "energy/charge attributes not found", + ), + )), + } + } + + pub fn get_battery_power_consumption(&self) -> Result { + if self.battery.join("power_now").exists() { + let content = std::fs::read_to_string(self.battery.join("power_now")) + .map_err(|e| PlatformError::Read("power_now".into(), e))?; + let power = content.trim().parse::().map_err(|e| { + PlatformError::Read( + "power_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + Ok(power / 1_000_000.0) + } else if self.battery.join("current_now").exists() && self.battery.join("voltage_now").exists() { + let current_str = std::fs::read_to_string(self.battery.join("current_now")) + .map_err(|e| PlatformError::Read("current_now".into(), e))?; + let voltage_str = std::fs::read_to_string(self.battery.join("voltage_now")) + .map_err(|e| PlatformError::Read("voltage_now".into(), e))?; + let current = current_str.trim().parse::().map_err(|e| { + PlatformError::Read( + "current_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + let voltage = voltage_str.trim().parse::().map_err(|e| { + PlatformError::Read( + "voltage_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + Ok((current * voltage) / 1_000_000_000.0) + } else { + Err(PlatformError::Read( + self.battery.to_string_lossy().into(), + std::io::Error::new( + std::io::ErrorKind::NotFound, + "power/current/voltage attributes not found", + ), + )) + } + } + + pub fn get_battery_status(&self) -> Result { + let path = self.battery.join("status"); + if !path.exists() { + return Err(PlatformError::Read( + path.to_string_lossy().into(), + std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"), + )); + } + let content = std::fs::read_to_string(&path) + .map_err(|e| PlatformError::Read(path.to_string_lossy().into(), e))?; + Ok(content.trim().to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::PathBuf; + + #[test] + fn test_battery_methods() { + let temp_dir = std::env::temp_dir().join("fake_battery"); + fs::create_dir_all(&temp_dir).unwrap(); + + // Write fake files + fs::write(temp_dir.join("cycle_count"), "42\n").unwrap(); + fs::write(temp_dir.join("energy_full"), "80000000\n").unwrap(); + fs::write(temp_dir.join("energy_full_design"), "100000000\n").unwrap(); + fs::write(temp_dir.join("power_now"), "15000000\n").unwrap(); + fs::write(temp_dir.join("status"), "Discharging\n").unwrap(); + + let power = AsusPower { + mains: PathBuf::new(), + battery: temp_dir.clone(), + usb: None, + }; + + assert!(power.has_battery()); + assert_eq!(power.get_battery_cycle_count().unwrap(), 42); + assert_eq!(power.get_battery_health().unwrap(), 80); + assert_eq!(power.get_battery_power_consumption().unwrap(), 15.0); + assert_eq!(power.get_battery_status().unwrap(), "Discharging"); + + // Clean up + fs::remove_dir_all(&temp_dir).ok(); + } } From ad982c59c9f1dde3efd4f1c4079b4085732e37f8 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:34:35 +0200 Subject: [PATCH 02/21] gui: Add Battery Information card to System page --- rog-control-center/ui/pages/system.slint | 80 ++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index 48f1edf0..cbe7a3cd 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -152,6 +152,10 @@ export global SystemPageData { in-out property ppt_enabled_available; in-out property ppt_enabled; callback cb_ppt_enabled(bool); + in-out property battery_health: -1; + in-out property battery_cycle_count: -1; + in-out property battery_power_consumption: -1.0; + in-out property battery_status: "Unknown"; } export component PageSystem inherits Rectangle { @@ -206,6 +210,82 @@ export component PageSystem inherits Rectangle { } } + if SystemPageData.battery_health != -1: VerticalLayout { + spacing: 6px; + padding-top: 6px; + padding-bottom: 6px; + Rectangle { + background: Palette.alternate-background; + border-color: Palette.border; + border-width: 1px; + border-radius: 2px; + + VerticalLayout { + padding: 10px; + spacing: 8px; + + Text { + font-size: 14px; + font-weight: 700; + color: Palette.accent-background; + text: @tr("Battery Information"); + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Battery Health (Capacity):"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_health + "%"; + font-weight: 700; + color: SystemPageData.battery_health > 80 ? #22c55e : (SystemPageData.battery_health > 50 ? #eab308 : #ef4444); + } + } + + if SystemPageData.battery_cycle_count != -1: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Cycle Count:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_cycle_count; + font-weight: 700; + color: Palette.control-foreground; + } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Status:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_status; + font-weight: 700; + color: Palette.control-foreground; + } + } + + if SystemPageData.battery_power_consumption != -1.0: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Power Draw:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.battery_power_consumption * 10) / 10 + " W"; + font-weight: 700; + color: SystemPageData.battery_status == "Discharging" ? #ef4444 : #22c55e; + } + } + } + } + } + if SystemPageData.platform_profile != -1: HorizontalLayout { spacing: 10px; SystemDropdown { From c54571eaa91e831dfd7a18e805fa76d98062daac Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:34:37 +0200 Subject: [PATCH 03/21] ui: Spawn background loop to poll and update battery stats --- rog-control-center/src/ui/setup_system.rs | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index 3de07613..07f20a86 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -37,6 +37,10 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { .set_charge_control_end_threshold(-1.0); ui.global::() .set_charge_control_enabled(false); + ui.global::().set_battery_health(-1); + ui.global::().set_battery_cycle_count(-1); + ui.global::().set_battery_power_consumption(-1.0); + ui.global::().set_battery_status("Unknown".into()); ui.global::().set_platform_profile(-1); ui.global::().set_panel_overdrive(-1); ui.global::().set_boot_sound(-1); @@ -69,6 +73,44 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { .set_charge_control_enabled(true); } } + + let handle = ui.as_weak(); + tokio::spawn(async move { + loop { + let power = rog_platform::power::AsusPower::new().ok(); + let (has_bat, health, cycles, consumption, status) = if let Some(ref p) = power { + if p.has_battery() { + let health = p.get_battery_health().unwrap_or(0) as i32; + let cycles = p.get_battery_cycle_count().unwrap_or(-1); + let consumption = p.get_battery_power_consumption().unwrap_or(-1.0); + let status = p.get_battery_status().unwrap_or_else(|_| "Unknown".to_string()); + (true, health, cycles, consumption, status) + } else { + (false, -1, -1, -1.0, "Unknown".to_string()) + } + } else { + (false, -1, -1, -1.0, "Unknown".to_string()) + }; + + let success = handle.upgrade_in_event_loop(move |ui| { + let data = ui.global::(); + if has_bat { + data.set_battery_health(health); + data.set_battery_cycle_count(cycles); + data.set_battery_power_consumption(consumption); + data.set_battery_status(status.into()); + } else { + data.set_battery_health(-1); + } + }); + + if success.is_err() { + break; + } + + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + } + }); } macro_rules! convert_value { From f30c7102ede15aa2b9e1784577b2cab27ef6de94 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:44:27 +0200 Subject: [PATCH 04/21] style: Apply cargo fmt --- rog-control-center/src/ui/setup_system.rs | 10 +++++++--- rog-platform/src/power.rs | 13 +++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index 07f20a86..ac43bebc 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -39,8 +39,10 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { .set_charge_control_enabled(false); ui.global::().set_battery_health(-1); ui.global::().set_battery_cycle_count(-1); - ui.global::().set_battery_power_consumption(-1.0); - ui.global::().set_battery_status("Unknown".into()); + ui.global::() + .set_battery_power_consumption(-1.0); + ui.global::() + .set_battery_status("Unknown".into()); ui.global::().set_platform_profile(-1); ui.global::().set_panel_overdrive(-1); ui.global::().set_boot_sound(-1); @@ -83,7 +85,9 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { let health = p.get_battery_health().unwrap_or(0) as i32; let cycles = p.get_battery_cycle_count().unwrap_or(-1); let consumption = p.get_battery_power_consumption().unwrap_or(-1.0); - let status = p.get_battery_status().unwrap_or_else(|_| "Unknown".to_string()); + let status = p + .get_battery_status() + .unwrap_or_else(|_| "Unknown".to_string()); (true, health, cycles, consumption, status) } else { (false, -1, -1, -1.0, "Unknown".to_string()) diff --git a/rog-platform/src/power.rs b/rog-platform/src/power.rs index 4af6cbc8..e937d486 100644 --- a/rog-platform/src/power.rs +++ b/rog-platform/src/power.rs @@ -127,13 +127,12 @@ impl AsusPower { } let content = std::fs::read_to_string(&path) .map_err(|e| PlatformError::Read(path.to_string_lossy().into(), e))?; - content - .trim() - .parse::() - .map_err(|e| PlatformError::Read( + content.trim().parse::().map_err(|e| { + PlatformError::Read( path.to_string_lossy().into(), std::io::Error::new(std::io::ErrorKind::InvalidData, e), - )) + ) + }) } pub fn get_battery_health(&self) -> Result { @@ -187,7 +186,9 @@ impl AsusPower { ) })?; Ok(power / 1_000_000.0) - } else if self.battery.join("current_now").exists() && self.battery.join("voltage_now").exists() { + } else if self.battery.join("current_now").exists() + && self.battery.join("voltage_now").exists() + { let current_str = std::fs::read_to_string(self.battery.join("current_now")) .map_err(|e| PlatformError::Read("current_now".into(), e))?; let voltage_str = std::fs::read_to_string(self.battery.join("voltage_now")) From 8f122faa2f3aaf95940913106a08bf8f2dbb3285 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:44:48 +0200 Subject: [PATCH 05/21] fix: Resolve clippy warning in get_battery_health --- rog-platform/src/power.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rog-platform/src/power.rs b/rog-platform/src/power.rs index e937d486..639fd82d 100644 --- a/rog-platform/src/power.rs +++ b/rog-platform/src/power.rs @@ -162,7 +162,7 @@ impl AsusPower { match (full, design) { (Some(f), Some(d)) if d > 0.0 => { - let health = (f / d * 100.0).round().min(100.0).max(0.0) as u8; + let health = (f / d * 100.0).round().clamp(0.0, 100.0) as u8; Ok(health) } _ => Err(PlatformError::Read( From 7a4a8f3d7d3da0fc6cb0def016befdf6fae13131 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:51:30 +0200 Subject: [PATCH 06/21] rog-platform: Add battery Wh capacity and time estimate calculations --- rog-platform/src/power.rs | 137 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/rog-platform/src/power.rs b/rog-platform/src/power.rs index 639fd82d..a114921b 100644 --- a/rog-platform/src/power.rs +++ b/rog-platform/src/power.rs @@ -229,6 +229,129 @@ impl AsusPower { .map_err(|e| PlatformError::Read(path.to_string_lossy().into(), e))?; Ok(content.trim().to_string()) } + + pub fn get_battery_remaining_energy_wh(&self) -> Result { + if self.battery.join("energy_now").exists() { + let content = std::fs::read_to_string(self.battery.join("energy_now")) + .map_err(|e| PlatformError::Read("energy_now".into(), e))?; + let val = content.trim().parse::().map_err(|e| { + PlatformError::Read( + "energy_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + Ok(val / 1_000_000.0) + } else if self.battery.join("charge_now").exists() + && self.battery.join("voltage_now").exists() + { + let charge_str = std::fs::read_to_string(self.battery.join("charge_now")) + .map_err(|e| PlatformError::Read("charge_now".into(), e))?; + let voltage_str = std::fs::read_to_string(self.battery.join("voltage_now")) + .map_err(|e| PlatformError::Read("voltage_now".into(), e))?; + let charge = charge_str.trim().parse::().map_err(|e| { + PlatformError::Read( + "charge_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + let voltage = voltage_str.trim().parse::().map_err(|e| { + PlatformError::Read( + "voltage_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + Ok((charge * voltage) / 1_000_000_000.0) + } else { + Err(PlatformError::Read( + self.battery.to_string_lossy().into(), + std::io::Error::new( + std::io::ErrorKind::NotFound, + "energy_now/charge_now attributes not found", + ), + )) + } + } + + pub fn get_battery_full_energy_wh(&self) -> Result { + if self.battery.join("energy_full").exists() { + let content = std::fs::read_to_string(self.battery.join("energy_full")) + .map_err(|e| PlatformError::Read("energy_full".into(), e))?; + let val = content.trim().parse::().map_err(|e| { + PlatformError::Read( + "energy_full".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + Ok(val / 1_000_000.0) + } else if self.battery.join("charge_full").exists() + && self.battery.join("voltage_now").exists() + { + let charge_str = std::fs::read_to_string(self.battery.join("charge_full")) + .map_err(|e| PlatformError::Read("charge_full".into(), e))?; + let voltage_str = std::fs::read_to_string(self.battery.join("voltage_now")) + .map_err(|e| PlatformError::Read("voltage_now".into(), e))?; + let charge = charge_str.trim().parse::().map_err(|e| { + PlatformError::Read( + "charge_full".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + let voltage = voltage_str.trim().parse::().map_err(|e| { + PlatformError::Read( + "voltage_now".into(), + std::io::Error::new(std::io::ErrorKind::InvalidData, e), + ) + })?; + Ok((charge * voltage) / 1_000_000_000.0) + } else { + Err(PlatformError::Read( + self.battery.to_string_lossy().into(), + std::io::Error::new( + std::io::ErrorKind::NotFound, + "energy_full/charge_full attributes not found", + ), + )) + } + } + + pub fn get_battery_time_estimate(&self) -> Result> { + let status = self.get_battery_status()?; + let is_charging = match status.as_str() { + "Charging" => true, + "Discharging" => false, + _ => return Ok(None), + }; + + let power_draw = self.get_battery_power_consumption().unwrap_or(0.0).abs(); + if power_draw < 0.1 { + return Ok(None); + } + + let energy_now = self.get_battery_remaining_energy_wh()?; + let energy_full = self.get_battery_full_energy_wh()?; + + let remaining_wh = if is_charging { + let limit = self.get_charge_control_end_threshold().unwrap_or(100) as f64; + let target_wh = energy_full * (limit / 100.0); + if energy_now >= target_wh { + return Ok(None); + } + target_wh - energy_now + } else { + energy_now + }; + + let hours_float = remaining_wh / power_draw as f64; + if hours_float < 0.0 || hours_float.is_nan() || hours_float.is_infinite() { + return Ok(None); + } + + let total_minutes = (hours_float * 60.0).round() as i32; + let hours = total_minutes / 60; + let minutes = total_minutes % 60; + + Ok(Some((is_charging, hours, minutes))) + } } #[cfg(test)] @@ -246,6 +369,7 @@ mod tests { fs::write(temp_dir.join("cycle_count"), "42\n").unwrap(); fs::write(temp_dir.join("energy_full"), "80000000\n").unwrap(); fs::write(temp_dir.join("energy_full_design"), "100000000\n").unwrap(); + fs::write(temp_dir.join("energy_now"), "45000000\n").unwrap(); fs::write(temp_dir.join("power_now"), "15000000\n").unwrap(); fs::write(temp_dir.join("status"), "Discharging\n").unwrap(); @@ -260,6 +384,19 @@ mod tests { assert_eq!(power.get_battery_health().unwrap(), 80); assert_eq!(power.get_battery_power_consumption().unwrap(), 15.0); assert_eq!(power.get_battery_status().unwrap(), "Discharging"); + assert_eq!(power.get_battery_remaining_energy_wh().unwrap(), 45.0); + assert_eq!(power.get_battery_full_energy_wh().unwrap(), 80.0); + assert_eq!( + power.get_battery_time_estimate().unwrap(), + Some((false, 3, 0)) + ); + + // Test charging estimation + fs::write(temp_dir.join("status"), "Charging\n").unwrap(); + assert_eq!( + power.get_battery_time_estimate().unwrap(), + Some((true, 2, 20)) + ); // Clean up fs::remove_dir_all(&temp_dir).ok(); From a4b72ebc9f7146e0caf4e29948b7f484ffa91fc4 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:51:32 +0200 Subject: [PATCH 07/21] gui: Render battery time estimate in System page --- rog-control-center/ui/pages/system.slint | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index cbe7a3cd..863256c1 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -156,6 +156,7 @@ export global SystemPageData { in-out property battery_cycle_count: -1; in-out property battery_power_consumption: -1.0; in-out property battery_status: "Unknown"; + in-out property battery_time_estimate: ""; } export component PageSystem inherits Rectangle { @@ -282,6 +283,19 @@ export component PageSystem inherits Rectangle { color: SystemPageData.battery_status == "Discharging" ? #ef4444 : #22c55e; } } + + if SystemPageData.battery_time_estimate != "": HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: SystemPageData.battery_status == "Charging" ? @tr("Time to Full:") : @tr("Time Remaining:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_time_estimate; + font-weight: 700; + color: Palette.control-foreground; + } + } } } } From 44ea4da688689a1f639f51e1b505f263a99e6807 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 08:51:34 +0200 Subject: [PATCH 08/21] ui: Update system page setup to poll and format battery time estimate --- rog-control-center/src/ui/setup_system.rs | 42 +++++++++++++++-------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index ac43bebc..bfe3c88d 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -43,6 +43,8 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { .set_battery_power_consumption(-1.0); ui.global::() .set_battery_status("Unknown".into()); + ui.global::() + .set_battery_time_estimate("".into()); ui.global::().set_platform_profile(-1); ui.global::().set_panel_overdrive(-1); ui.global::().set_boot_sound(-1); @@ -80,21 +82,32 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { tokio::spawn(async move { loop { let power = rog_platform::power::AsusPower::new().ok(); - let (has_bat, health, cycles, consumption, status) = if let Some(ref p) = power { - if p.has_battery() { - let health = p.get_battery_health().unwrap_or(0) as i32; - let cycles = p.get_battery_cycle_count().unwrap_or(-1); - let consumption = p.get_battery_power_consumption().unwrap_or(-1.0); - let status = p - .get_battery_status() - .unwrap_or_else(|_| "Unknown".to_string()); - (true, health, cycles, consumption, status) + let (has_bat, health, cycles, consumption, status, estimate_str) = + if let Some(ref p) = power { + if p.has_battery() { + let health = p.get_battery_health().unwrap_or(0) as i32; + let cycles = p.get_battery_cycle_count().unwrap_or(-1); + let consumption = p.get_battery_power_consumption().unwrap_or(-1.0); + let status = p + .get_battery_status() + .unwrap_or_else(|_| "Unknown".to_string()); + let estimate = p.get_battery_time_estimate().ok().flatten(); + let est_str = if let Some((_, h, m)) = estimate { + if h > 0 { + format!("{}h {}m", h, m) + } else { + format!("{}m", m) + } + } else { + "".to_string() + }; + (true, health, cycles, consumption, status, est_str) + } else { + (false, -1, -1, -1.0, "Unknown".to_string(), "".to_string()) + } } else { - (false, -1, -1, -1.0, "Unknown".to_string()) - } - } else { - (false, -1, -1, -1.0, "Unknown".to_string()) - }; + (false, -1, -1, -1.0, "Unknown".to_string(), "".to_string()) + }; let success = handle.upgrade_in_event_loop(move |ui| { let data = ui.global::(); @@ -103,6 +116,7 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { data.set_battery_cycle_count(cycles); data.set_battery_power_consumption(consumption); data.set_battery_status(status.into()); + data.set_battery_time_estimate(estimate_str.into()); } else { data.set_battery_health(-1); } From a99e19c748d8e24fdbdc482a3dad081be551a06e Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:08:46 +0200 Subject: [PATCH 09/21] Expose hardware monitoring properties in system.slint --- rog-control-center/ui/pages/system.slint | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index 863256c1..da089107 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -157,6 +157,15 @@ export global SystemPageData { in-out property battery_power_consumption: -1.0; in-out property battery_status: "Unknown"; in-out property battery_time_estimate: ""; + in-out property cpu_temp_val: -1.0; + in-out property gpu_temp_val: -1.0; + in-out property cpu_usage_val: -1.0; + in-out property gpu_usage_val: -1.0; + in-out property ram_usage_val: -1.0; + in-out property cpu_freq_mhz: -1.0; + in-out property cpu_fan_rpm: -1; + in-out property gpu_fan_rpm: -1; + in-out property mid_fan_rpm: -1; } export component PageSystem inherits Rectangle { @@ -300,6 +309,19 @@ export component PageSystem inherits Rectangle { } } + if SystemPageData.cpu_usage_val != -1.0: VerticalLayout { + spacing: 6px; + Text { text: "CPU Temp: " + Math.round(SystemPageData.cpu_temp_val) + " C"; color: Palette.control-foreground; } + Text { text: "GPU Temp: " + Math.round(SystemPageData.gpu_temp_val) + " C"; color: Palette.control-foreground; } + Text { text: "CPU Usage: " + Math.round(SystemPageData.cpu_usage_val) + "%"; color: Palette.control-foreground; } + Text { text: "GPU Usage: " + Math.round(SystemPageData.gpu_usage_val) + "%"; color: Palette.control-foreground; } + Text { text: "RAM Usage: " + Math.round(SystemPageData.ram_usage_val) + "%"; color: Palette.control-foreground; } + Text { text: "CPU Freq: " + Math.round(SystemPageData.cpu_freq_mhz) + " MHz"; color: Palette.control-foreground; } + Text { text: "CPU Fan: " + SystemPageData.cpu_fan_rpm + " RPM"; color: Palette.control-foreground; } + Text { text: "GPU Fan: " + SystemPageData.gpu_fan_rpm + " RPM"; color: Palette.control-foreground; } + Text { text: "Mid Fan: " + SystemPageData.mid_fan_rpm + " RPM"; color: Palette.control-foreground; } + } + if SystemPageData.platform_profile != -1: HorizontalLayout { spacing: 10px; SystemDropdown { From 520d016d6f8c7e90e9f260aa14580b86b2214565 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:08:47 +0200 Subject: [PATCH 10/21] Implement live hardware monitoring backend in setup_system.rs Fix Rust 2024 match ergonomics compile error in setup_system.rs --- rog-control-center/src/ui/setup_system.rs | 205 +++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index bfe3c88d..c686cb4a 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -80,6 +80,7 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { let handle = ui.as_weak(); tokio::spawn(async move { + let mut prev_ticks = read_cpu_ticks(); loop { let power = rog_platform::power::AsusPower::new().ok(); let (has_bat, health, cycles, consumption, status, estimate_str) = @@ -109,6 +110,27 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { (false, -1, -1, -1.0, "Unknown".to_string(), "".to_string()) }; + let cpu_temp = get_cpu_temp(); + let gpu_temp = get_gpu_temp(); + let (cpu_fan, gpu_fan, mid_fan) = get_fan_rpms(); + let cpu_freq = get_cpu_frequency_mhz(); + let ram_usage = get_ram_usage_pct(); + let gpu_usage = get_gpu_usage_pct(); + + let curr_ticks = read_cpu_ticks(); + let cpu_usage = if let (Some(p), Some(c)) = (&prev_ticks, &curr_ticks) { + let idle_diff = c.idle.saturating_sub(p.idle) as f32; + let total_diff = c.total.saturating_sub(p.total) as f32; + if total_diff > 0.0 { + ((1.0 - (idle_diff / total_diff)) * 100.0).clamp(0.0, 100.0) + } else { + 0.0 + } + } else { + 0.0 + }; + prev_ticks = curr_ticks; + let success = handle.upgrade_in_event_loop(move |ui| { let data = ui.global::(); if has_bat { @@ -120,13 +142,22 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { } else { data.set_battery_health(-1); } + data.set_cpu_temp_val(cpu_temp); + data.set_gpu_temp_val(gpu_temp); + data.set_cpu_usage_val(cpu_usage); + data.set_gpu_usage_val(gpu_usage); + data.set_ram_usage_val(ram_usage); + data.set_cpu_freq_mhz(cpu_freq); + data.set_cpu_fan_rpm(cpu_fan); + data.set_gpu_fan_rpm(gpu_fan); + data.set_mid_fan_rpm(mid_fan); }); if success.is_err() { break; } - tokio::time::sleep(std::time::Duration::from_secs(5)).await; + tokio::time::sleep(std::time::Duration::from_secs(2)).await; } }); } @@ -806,3 +837,175 @@ pub fn setup_system_page_callbacks(ui: &MainWindow, _states: Arc>) .ok(); }); } + +fn get_cpu_temp() -> f32 { + if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { + for entry in entries.flatten() { + let path = entry.path(); + if let Ok(name) = std::fs::read_to_string(path.join("name")) { + let name = name.trim(); + if name == "k10temp" || name == "coretemp" || name == "zenpower" { + if let Ok(temp_str) = std::fs::read_to_string(path.join("temp1_input")) { + if let Ok(temp_val) = temp_str.trim().parse::() { + return temp_val / 1000.0; + } + } + } + } + } + } + if let Ok(temp_str) = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp") { + if let Ok(temp_val) = temp_str.trim().parse::() { + return temp_val / 1000.0; + } + } + 0.0 +} + +fn get_gpu_temp() -> f32 { + if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { + for entry in entries.flatten() { + let path = entry.path(); + if let Ok(name) = std::fs::read_to_string(path.join("name")) { + let name = name.trim(); + if name == "amdgpu" || name == "nouveau" || name == "nvidia" { + if let Ok(temp_str) = std::fs::read_to_string(path.join("temp1_input")) { + if let Ok(temp_val) = temp_str.trim().parse::() { + return temp_val / 1000.0; + } + } + } + } + } + } + 0.0 +} + +fn get_fan_rpms() -> (i32, i32, i32) { + let mut cpu = 0; + let mut gpu = 0; + let mut mid = 0; + if let Ok(entries) = std::fs::read_dir("/sys/class/hwmon") { + for entry in entries.flatten() { + let path = entry.path(); + if let Ok(name) = std::fs::read_to_string(path.join("name")) { + if name.trim() == "asus" { + if let Ok(v) = std::fs::read_to_string(path.join("fan1_input")) { + cpu = v.trim().parse().unwrap_or(0); + } + if let Ok(v) = std::fs::read_to_string(path.join("fan2_input")) { + gpu = v.trim().parse().unwrap_or(0); + } + if let Ok(v) = std::fs::read_to_string(path.join("fan3_input")) { + mid = v.trim().parse().unwrap_or(0); + } + break; + } + } + } + } + (cpu, gpu, mid) +} + +fn get_cpu_frequency_mhz() -> f32 { + let mut total_freq = 0.0; + let mut count = 0; + if let Ok(entries) = std::fs::read_dir("/sys/devices/system/cpu") { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().into_owned(); + if name.starts_with("cpu") && name[3..].chars().all(|c| c.is_ascii_digit()) { + let freq_path = entry.path().join("cpufreq/scaling_cur_freq"); + if let Ok(freq_str) = std::fs::read_to_string(freq_path) { + if let Ok(freq_khz) = freq_str.trim().parse::() { + total_freq += freq_khz / 1000.0; + count += 1; + } + } + } + } + } + if count == 0 { + if let Ok(cpuinfo) = std::fs::read_to_string("/proc/cpuinfo") { + for line in cpuinfo.lines() { + if line.starts_with("cpu MHz") { + if let Some(pos) = line.find(':') { + if let Ok(val) = line[pos + 1..].trim().parse::() { + total_freq += val; + count += 1; + } + } + } + } + } + } + if count > 0 { + total_freq / count as f32 + } else { + 0.0 + } +} + +fn get_ram_usage_pct() -> f32 { + if let Ok(meminfo) = std::fs::read_to_string("/proc/meminfo") { + let mut total = 0.0; + let mut available = 0.0; + for line in meminfo.lines() { + if line.starts_with("MemTotal:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if let Some(val) = parts.get(1) { + total = val.parse::().unwrap_or(0.0); + } + } else if line.starts_with("MemAvailable:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if let Some(val) = parts.get(1) { + available = val.parse::().unwrap_or(0.0); + } + } + } + if total > 0.0 { + return ((total - available) / total) * 100.0; + } + } + 0.0 +} + +struct CpuTicks { + idle: u64, + total: u64, +} + +fn read_cpu_ticks() -> Option { + let stat = std::fs::read_to_string("/proc/stat").ok()?; + let first_line = stat.lines().next()?; + if first_line.starts_with("cpu ") { + let parts: Vec<&str> = first_line.split_whitespace().collect(); + let mut total = 0u64; + let mut idle = 0u64; + for (i, part) in parts.iter().skip(1).enumerate() { + if let Ok(ticks) = part.parse::() { + total += ticks; + if i == 3 || i == 4 { + idle += ticks; + } + } + } + return Some(CpuTicks { idle, total }); + } + None +} + +fn get_gpu_usage_pct() -> f32 { + if let Ok(entries) = std::fs::read_dir("/sys/class/drm") { + for entry in entries.flatten() { + let path = entry.path().join("device/gpu_busy_percent"); + if path.exists() { + if let Ok(val_str) = std::fs::read_to_string(path) { + if let Ok(val) = val_str.trim().parse::() { + return val; + } + } + } + } + } + 0.0 +} From aa7988ee5e669190d8eb53e4d6e70ca71ece8023 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:09:31 +0200 Subject: [PATCH 11/21] Redesign system page layout with modern cards and icons --- rog-control-center/ui/pages/system.slint | 309 ++++++++++++++++++----- 1 file changed, 242 insertions(+), 67 deletions(-) diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index da089107..815b79f3 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -220,108 +220,283 @@ export component PageSystem inherits Rectangle { } } - if SystemPageData.battery_health != -1: VerticalLayout { - spacing: 6px; - padding-top: 6px; - padding-bottom: 6px; - Rectangle { - background: Palette.alternate-background; - border-color: Palette.border; - border-width: 1px; - border-radius: 2px; + if SystemPageData.battery_health != -1: Rectangle { + background: Palette.alternate-background; + border-color: Palette.border; + border-width: 1px; + border-radius: 4px; + + VerticalLayout { + padding: 12px; + spacing: 8px; - VerticalLayout { - padding: 10px; + HorizontalLayout { + alignment: LayoutAlignment.start; spacing: 8px; - Text { font-size: 14px; font-weight: 700; color: Palette.accent-background; text: @tr("Battery Information"); } - - HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Battery Health (Capacity):"); - color: Palette.control-foreground; - } - Text { - text: SystemPageData.battery_health + "%"; - font-weight: 700; - color: SystemPageData.battery_health > 80 ? #22c55e : (SystemPageData.battery_health > 50 ? #eab308 : #ef4444); - } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Battery Health (Capacity):"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_health + "%"; + font-weight: 700; + color: SystemPageData.battery_health > 80 ? #22c55e : (SystemPageData.battery_health > 50 ? #eab308 : #ef4444); + } + } + + if SystemPageData.battery_cycle_count != -1: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Cycle Count:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_cycle_count; + font-weight: 700; + color: Palette.control-foreground; } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Status:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_status; + font-weight: 700; + color: Palette.control-foreground; + } + } + + if SystemPageData.battery_power_consumption != -1.0: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Power Draw:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.battery_power_consumption * 10) / 10 + " W"; + font-weight: 700; + color: SystemPageData.battery_status == "Discharging" ? #ef4444 : #22c55e; + } + } + + if SystemPageData.battery_time_estimate != "": HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: SystemPageData.battery_status == "Charging" ? @tr("Time to Full:") : @tr("Time Remaining:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_time_estimate; + font-weight: 700; + color: Palette.control-foreground; + } + } + } + } + + + if SystemPageData.cpu_usage_val != -1.0: Rectangle { + background: Palette.alternate-background; + border-color: Palette.border; + border-width: 1px; + border-radius: 4px; + + VerticalLayout { + padding: 12px; + spacing: 12px; + + HorizontalLayout { + alignment: LayoutAlignment.start; + spacing: 8px; + Text { + font-size: 14px; + font-weight: 700; + color: Palette.accent-background; + text: @tr("Hardware Monitor"); + } + } + + HorizontalLayout { + spacing: 16px; - if SystemPageData.battery_cycle_count != -1: HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Cycle Count:"); - color: Palette.control-foreground; - } + // Left Column: CPU & Memory + VerticalLayout { + width: 50%; + spacing: 8px; + Text { - text: SystemPageData.battery_cycle_count; + text: @tr("CPU Status"); font-weight: 700; color: Palette.control-foreground; + font-size: 12px; } - } - - HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Status:"); - color: Palette.control-foreground; + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Temperature:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.cpu_temp_val) + " °C"; + font-weight: 700; + color: SystemPageData.cpu_temp_val > 80 ? #ef4444 : (SystemPageData.cpu_temp_val > 65 ? #eab308 : #22c55e); + } } - Text { - text: SystemPageData.battery_status; - font-weight: 700; - color: Palette.control-foreground; + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Usage:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.cpu_usage_val) + "%"; + font-weight: 700; + color: Palette.control-foreground; + } } - } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Frequency:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.cpu_freq_mhz) + " MHz"; + font-weight: 700; + color: Palette.control-foreground; + } + } + + Rectangle { height: 4px; } // divider spacer - if SystemPageData.battery_power_consumption != -1.0: HorizontalLayout { - alignment: LayoutAlignment.space-between; Text { - text: @tr("Power Draw:"); + text: @tr("Memory Status"); + font-weight: 700; color: Palette.control-foreground; + font-size: 12px; } - Text { - text: Math.round(SystemPageData.battery_power_consumption * 10) / 10 + " W"; - font-weight: 700; - color: SystemPageData.battery_status == "Discharging" ? #ef4444 : #22c55e; + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("RAM Usage:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.ram_usage_val) + "%"; + font-weight: 700; + color: Palette.control-foreground; + } } } - if SystemPageData.battery_time_estimate != "": HorizontalLayout { - alignment: LayoutAlignment.space-between; + // Right Column: GPU & Fans + VerticalLayout { + width: 50%; + spacing: 8px; + Text { - text: SystemPageData.battery_status == "Charging" ? @tr("Time to Full:") : @tr("Time Remaining:"); + text: @tr("GPU Status"); + font-weight: 700; color: Palette.control-foreground; + font-size: 12px; + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Temperature:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.gpu_temp_val > 0.0 ? (Math.round(SystemPageData.gpu_temp_val) + " °C") : @tr("N/A"); + font-weight: 700; + color: SystemPageData.gpu_temp_val > 80 ? #ef4444 : (SystemPageData.gpu_temp_val > 65 ? #eab308 : (SystemPageData.gpu_temp_val > 0.0 ? #22c55e : Palette.control-foreground)); + } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Usage:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.gpu_usage_val >= 0.0 ? (Math.round(SystemPageData.gpu_usage_val) + "%") : @tr("N/A"); + font-weight: 700; + color: Palette.control-foreground; + } } + + Rectangle { height: 4px; } // divider spacer + Text { - text: SystemPageData.battery_time_estimate; + text: @tr("Fan Speeds"); font-weight: 700; color: Palette.control-foreground; + font-size: 12px; + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("CPU Fan:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.cpu_fan_rpm + " RPM"; + font-weight: 700; + color: Palette.control-foreground; + } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("GPU Fan:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.gpu_fan_rpm + " RPM"; + font-weight: 700; + color: Palette.control-foreground; + } + } + + if SystemPageData.mid_fan_rpm > 0: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Mid Fan:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.mid_fan_rpm + " RPM"; + font-weight: 700; + color: Palette.control-foreground; + } } } } } } - if SystemPageData.cpu_usage_val != -1.0: VerticalLayout { - spacing: 6px; - Text { text: "CPU Temp: " + Math.round(SystemPageData.cpu_temp_val) + " C"; color: Palette.control-foreground; } - Text { text: "GPU Temp: " + Math.round(SystemPageData.gpu_temp_val) + " C"; color: Palette.control-foreground; } - Text { text: "CPU Usage: " + Math.round(SystemPageData.cpu_usage_val) + "%"; color: Palette.control-foreground; } - Text { text: "GPU Usage: " + Math.round(SystemPageData.gpu_usage_val) + "%"; color: Palette.control-foreground; } - Text { text: "RAM Usage: " + Math.round(SystemPageData.ram_usage_val) + "%"; color: Palette.control-foreground; } - Text { text: "CPU Freq: " + Math.round(SystemPageData.cpu_freq_mhz) + " MHz"; color: Palette.control-foreground; } - Text { text: "CPU Fan: " + SystemPageData.cpu_fan_rpm + " RPM"; color: Palette.control-foreground; } - Text { text: "GPU Fan: " + SystemPageData.gpu_fan_rpm + " RPM"; color: Palette.control-foreground; } - Text { text: "Mid Fan: " + SystemPageData.mid_fan_rpm + " RPM"; color: Palette.control-foreground; } - } - if SystemPageData.platform_profile != -1: HorizontalLayout { spacing: 10px; SystemDropdown { From bd81926a7badf438c7f0a93847008a8dd3dd4ef6 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:29 +0200 Subject: [PATCH 12/21] Add serde_json to rog-control-center dependencies --- rog-control-center/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/rog-control-center/Cargo.toml b/rog-control-center/Cargo.toml index 97c56b44..02b846b4 100644 --- a/rog-control-center/Cargo.toml +++ b/rog-control-center/Cargo.toml @@ -44,6 +44,7 @@ notify-rust.workspace = true concat-idents.workspace = true futures-util.workspace = true thiserror.workspace = true +serde_json = "1.0" [dependencies.slint] git = "https://github.com/slint-ui/slint.git" From 8e2733608c473de6779b585eb741872da969ede1 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:30 +0200 Subject: [PATCH 13/21] Update Cargo.lock for serde_json --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 2d9d73cc..a74eb73c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4703,6 +4703,7 @@ dependencies = [ "rog_profiles", "rog_slash", "serde", + "serde_json", "slint", "slint-build", "thiserror 2.0.18", From 01f027fefdeb49912dc53cc2c71e791be91e9327 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:30 +0200 Subject: [PATCH 14/21] Add auto_refresh_rate configuration to Config --- rog-control-center/src/config.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rog-control-center/src/config.rs b/rog-control-center/src/config.rs index ef8207f2..82045cec 100644 --- a/rog-control-center/src/config.rs +++ b/rog-control-center/src/config.rs @@ -13,6 +13,8 @@ pub struct Config { pub run_in_background: bool, pub startup_in_background: bool, pub enable_tray_icon: bool, + #[serde(default)] + pub auto_refresh_rate: bool, pub ac_command: String, pub bat_command: String, pub dark_mode: bool, @@ -30,6 +32,7 @@ impl Default for Config { run_in_background: true, startup_in_background: false, enable_tray_icon: true, + auto_refresh_rate: false, dark_mode: true, start_fullscreen: false, fullscreen_width: 1920, @@ -86,6 +89,7 @@ impl From for Config { run_in_background: c.run_in_background, startup_in_background: c.startup_in_background, enable_tray_icon: true, + auto_refresh_rate: false, ac_command: c.ac_command, bat_command: c.bat_command, dark_mode: true, From f12f9704507846c82b263951f07585d90a9f1bce Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:33 +0200 Subject: [PATCH 15/21] Add Auto-switch refresh rate toggle to App Settings UI --- rog-control-center/ui/pages/app_settings.slint | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rog-control-center/ui/pages/app_settings.slint b/rog-control-center/ui/pages/app_settings.slint index 95919868..937b6b69 100644 --- a/rog-control-center/ui/pages/app_settings.slint +++ b/rog-control-center/ui/pages/app_settings.slint @@ -10,6 +10,8 @@ export global AppSettingsPageData { callback set_enable_tray_icon(bool); in-out property enable_dgpu_notifications; callback set_enable_dgpu_notifications(bool); + in-out property auto_refresh_rate; + callback set_auto_refresh_rate(bool); } export component PageAppSettings inherits VerticalLayout { @@ -51,6 +53,13 @@ export component PageAppSettings inherits VerticalLayout { } } + SystemToggle { + text: @tr("Auto-switch refresh rate on battery"); + checked <=> AppSettingsPageData.auto_refresh_rate; + toggled => { + AppSettingsPageData.set_auto_refresh_rate(AppSettingsPageData.auto_refresh_rate) + } + } Text { color: Palette.accent-background; text: " WIP: some features like notifications are not complete"; From 519b57298063f517b9d032f733bee8d831330885 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:33 +0200 Subject: [PATCH 16/21] Bind auto_refresh_rate toggle in App Settings controller --- rog-control-center/src/ui/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rog-control-center/src/ui/mod.rs b/rog-control-center/src/ui/mod.rs index b0d0b10c..aac4da7f 100644 --- a/rog-control-center/src/ui/mod.rs +++ b/rog-control-center/src/ui/mod.rs @@ -211,11 +211,19 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc>) { lock.write(); } }); + let config_copy = config.clone(); + global.on_set_auto_refresh_rate(move |enable| { + if let Ok(mut lock) = config_copy.try_lock() { + lock.auto_refresh_rate = enable; + lock.write(); + } + }); 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_auto_refresh_rate(lock.auto_refresh_rate); } } From fd078208f03c26280d63ebeab038e79af4324a9c Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:33 +0200 Subject: [PATCH 17/21] Add Display Settings card to System UI --- rog-control-center/ui/pages/system.slint | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index 815b79f3..a6f6a2d3 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -166,6 +166,10 @@ export global SystemPageData { in-out property cpu_fan_rpm: -1; in-out property gpu_fan_rpm: -1; in-out property mid_fan_rpm: -1; + in-out property refresh_rate_current: ""; + in-out property <[string]> refresh_rate_choices: []; + in-out property refresh_rate_active_idx: -1; + callback cb_refresh_rate_changed(int); } export component PageSystem inherits Rectangle { @@ -308,6 +312,49 @@ export component PageSystem inherits Rectangle { } } + if SystemPageData.refresh_rate_active_idx != -1: Rectangle { + background: Palette.alternate-background; + border-color: Palette.border; + border-width: 1px; + border-radius: 4px; + + VerticalLayout { + padding: 12px; + spacing: 8px; + + HorizontalLayout { + alignment: LayoutAlignment.start; + spacing: 8px; + Text { + font-size: 14px; + font-weight: 700; + color: Palette.accent-background; + text: @tr("Display Settings"); + } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + spacing: 12px; + + Text { + text: @tr("Refresh Rate:"); + color: Palette.control-foreground; + vertical-alignment: TextVerticalAlignment.center; + } + + SystemDropdown { + combo_min_width: 150px; + current_index <=> SystemPageData.refresh_rate_active_idx; + current_value: SystemPageData.refresh_rate_choices[SystemPageData.refresh_rate_active_idx]; + model <=> SystemPageData.refresh_rate_choices; + selected => { + SystemPageData.cb_refresh_rate_changed(SystemPageData.refresh_rate_active_idx) + } + } + } + } + } if SystemPageData.cpu_usage_val != -1.0: Rectangle { background: Palette.alternate-background; From 6e3e87adec2c9c2797ca99bc95e70df1df66c593 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:11:33 +0200 Subject: [PATCH 18/21] Implement refresh rate auto-switching logic on power state change --- rog-control-center/src/ui/setup_system.rs | 158 +++++++++++++++++++++- 1 file changed, 157 insertions(+), 1 deletion(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index c686cb4a..30b7fbc1 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -20,7 +20,7 @@ const MINMAX: AttrMinMax = AttrMinMax { current: -1.0, }; -pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { +pub fn setup_system_page(ui: &MainWindow, config: Arc>) { let conn = zbus::blocking::Connection::system() .map_err(|e| error!("DBus system connection failed: {e:?}")) .unwrap(); @@ -78,9 +78,23 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { } } + let handle_copy = ui.as_weak(); + ui.global::().on_cb_refresh_rate_changed(move |idx| { + if let Some((name, _, modes)) = get_kscreen_info() { + if let Some(mode) = modes.get(idx as usize) { + set_kscreen_mode(&name, &mode.0); + if let Some(handle) = handle_copy.upgrade() { + handle.global::().set_refresh_rate_active_idx(idx); + } + } + } + }); + let handle = ui.as_weak(); + let config_clone = config.clone(); tokio::spawn(async move { let mut prev_ticks = read_cpu_ticks(); + let mut prev_online = None; loop { let power = rog_platform::power::AsusPower::new().ok(); let (has_bat, health, cycles, consumption, status, estimate_str) = @@ -131,6 +145,66 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { }; prev_ticks = curr_ticks; + // Refresh rate auto-switching logic + let kscreen_info = get_kscreen_info(); + let mut refresh_rate_choices_strings = Vec::new(); + let mut active_idx = -1; + let mut output_name = String::new(); + let mut current_mode_id = String::new(); + let mut available_modes_store = Vec::new(); + if let Some((name, curr_id, modes)) = kscreen_info { + output_name = name; + current_mode_id = curr_id; + available_modes_store = modes.clone(); + for (i, m) in modes.iter().enumerate() { + refresh_rate_choices_strings.push(format!("{:.2} Hz", m.2)); + if m.0 == current_mode_id { + active_idx = i as i32; + } + } + } + + let curr_online = power.as_ref().map(|p| p.get_online().unwrap_or(1) == 1).unwrap_or(true); + if let Some(prev) = prev_online { + if prev != curr_online { + let auto_switch_enabled = if let Ok(lock) = config_clone.try_lock() { + lock.auto_refresh_rate + } else { + false + }; + if auto_switch_enabled && !available_modes_store.is_empty() { + let target_mode = if curr_online { + available_modes_store.last() + } else { + available_modes_store.first() + }; + if let Some(mode) = target_mode { + if mode.0 != current_mode_id { + set_kscreen_mode(&output_name, &mode.0); + current_mode_id = mode.0.clone(); + if let Some(pos) = available_modes_store.iter().position(|x| x.0 == mode.0) { + active_idx = pos as i32; + } + let msg = if curr_online { + format!("AC connected. Refresh rate set to {:.2} Hz.", mode.2) + } else { + format!("AC disconnected. Refresh rate set to {:.2} Hz for power saving.", mode.2) + }; + tokio::spawn(async move { + let _ = notify_rust::Notification::new() + .summary("Display Refresh Rate") + .body(&msg) + .icon("rog-control-center") + .show(); + }); + } + } + } + } + } + prev_online = Some(curr_online); + + let choices_strings = refresh_rate_choices_strings.clone(); let success = handle.upgrade_in_event_loop(move |ui| { let data = ui.global::(); if has_bat { @@ -151,6 +225,32 @@ pub fn setup_system_page(ui: &MainWindow, _config: Arc>) { data.set_cpu_fan_rpm(cpu_fan); data.set_gpu_fan_rpm(gpu_fan); data.set_mid_fan_rpm(mid_fan); + + // Refresh rate properties + if active_idx != -1 { + if data.get_refresh_rate_active_idx() != active_idx { + data.set_refresh_rate_active_idx(active_idx); + } + let ui_choices = data.get_refresh_rate_choices(); + let mut choices_changed = ui_choices.row_count() != choices_strings.len(); + if !choices_changed { + for i in 0..ui_choices.row_count() { + if ui_choices.row_data(i) != Some(choices_strings[i].clone().into()) { + choices_changed = true; + break; + } + } + } + if choices_changed { + let model = slint::VecModel::default(); + for choice in &choices_strings { + model.push(choice.clone().into()); + } + data.set_refresh_rate_choices(slint::ModelRc::new(model)); + } + } else { + data.set_refresh_rate_active_idx(-1); + } }); if success.is_err() { @@ -1009,3 +1109,59 @@ fn get_gpu_usage_pct() -> f32 { } 0.0 } + +#[derive(serde::Deserialize, Debug)] +struct KScreenOutput { + name: String, + #[serde(rename = "currentModeId")] + current_mode_id: String, + modes: Vec, + #[serde(rename = "type")] + output_type: i32, +} + +#[derive(serde::Deserialize, Debug)] +struct KScreenMode { + id: String, + name: String, + #[serde(rename = "refreshRate")] + refresh_rate: f64, +} + +#[derive(serde::Deserialize, Debug)] +struct KScreenConfig { + outputs: Vec, +} + +fn get_kscreen_info() -> Option<(String, String, Vec<(String, String, f64)>)> { + let output = std::process::Command::new("kscreen-doctor") + .arg("-j") + .output() + .ok()?; + if !output.status.success() { + return None; + } + let config: KScreenConfig = serde_json::from_slice(&output.stdout).ok()?; + let panel = config.outputs.iter().find(|o| o.output_type == 7 || o.name.starts_with("eDP"))?; + let current_mode = panel.modes.iter().find(|m| m.id == panel.current_mode_id)?; + let current_res = current_mode.name.split('@').next()?.trim(); + + let mut available_rates = Vec::new(); + for m in &panel.modes { + if let Some(res) = m.name.split('@').next() { + if res.trim() == current_res { + available_rates.push((m.id.clone(), m.name.clone(), m.refresh_rate)); + } + } + } + available_rates.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)); + + Some((panel.name.clone(), current_mode.id.clone(), available_rates)) +} + +fn set_kscreen_mode(output_name: &str, mode_id: &str) { + let arg = format!("output.{}.mode.{}", output_name, mode_id); + let _ = std::process::Command::new("kscreen-doctor") + .arg(arg) + .output(); +} From c8443b78c0e4925c76843d9e0002036b009b4c20 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 09:18:17 +0200 Subject: [PATCH 19/21] Refactor display settings backend to be DE and compositor agnostic --- rog-control-center/src/ui/setup_system.rs | 118 +++++++++++++++++++++- 1 file changed, 113 insertions(+), 5 deletions(-) diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index 30b7fbc1..eb2ad576 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -78,11 +78,12 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { } } + let backend = detect_display_backend(); let handle_copy = ui.as_weak(); ui.global::().on_cb_refresh_rate_changed(move |idx| { - if let Some((name, _, modes)) = get_kscreen_info() { + if let Some((name, _, modes)) = get_display_info(backend) { if let Some(mode) = modes.get(idx as usize) { - set_kscreen_mode(&name, &mode.0); + set_display_mode(backend, &name, &mode.0); if let Some(handle) = handle_copy.upgrade() { handle.global::().set_refresh_rate_active_idx(idx); } @@ -146,13 +147,13 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { prev_ticks = curr_ticks; // Refresh rate auto-switching logic - let kscreen_info = get_kscreen_info(); + let display_info = get_display_info(backend); let mut refresh_rate_choices_strings = Vec::new(); let mut active_idx = -1; let mut output_name = String::new(); let mut current_mode_id = String::new(); let mut available_modes_store = Vec::new(); - if let Some((name, curr_id, modes)) = kscreen_info { + if let Some((name, curr_id, modes)) = display_info { output_name = name; current_mode_id = curr_id; available_modes_store = modes.clone(); @@ -180,7 +181,7 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { }; if let Some(mode) = target_mode { if mode.0 != current_mode_id { - set_kscreen_mode(&output_name, &mode.0); + set_display_mode(backend, &output_name, &mode.0); current_mode_id = mode.0.clone(); if let Some(pos) = available_modes_store.iter().position(|x| x.0 == mode.0) { active_idx = pos as i32; @@ -1110,6 +1111,39 @@ fn get_gpu_usage_pct() -> f32 { 0.0 } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WaylandDisplayBackend { + KScreenDoctor, + WlrRandr, + None, +} + +fn detect_display_backend() -> WaylandDisplayBackend { + if std::process::Command::new("kscreen-doctor").arg("--help").output().is_ok() { + WaylandDisplayBackend::KScreenDoctor + } else if std::process::Command::new("wlr-randr").arg("--help").output().is_ok() { + WaylandDisplayBackend::WlrRandr + } else { + WaylandDisplayBackend::None + } +} + +fn get_display_info(backend: WaylandDisplayBackend) -> Option<(String, String, Vec<(String, String, f64)>)> { + match backend { + WaylandDisplayBackend::KScreenDoctor => get_kscreen_info(), + WaylandDisplayBackend::WlrRandr => get_wlr_info(), + WaylandDisplayBackend::None => None, + } +} + +fn set_display_mode(backend: WaylandDisplayBackend, output_name: &str, mode_id: &str) { + match backend { + WaylandDisplayBackend::KScreenDoctor => set_kscreen_mode(output_name, mode_id), + WaylandDisplayBackend::WlrRandr => set_wlr_mode(output_name, mode_id), + WaylandDisplayBackend::None => {} + } +} + #[derive(serde::Deserialize, Debug)] struct KScreenOutput { name: String, @@ -1165,3 +1199,77 @@ fn set_kscreen_mode(output_name: &str, mode_id: &str) { .arg(arg) .output(); } + +fn get_wlr_info() -> Option<(String, String, Vec<(String, String, f64)>)> { + let output = std::process::Command::new("wlr-randr") + .output() + .ok()?; + if !output.status.success() { + return None; + } + let stdout = String::from_utf8_lossy(&output.stdout); + + let mut current_output = String::new(); + let mut is_edp = false; + let mut current_mode_str = String::new(); + let mut modes = Vec::new(); + + for line in stdout.lines() { + let trimmed = line.trim(); + if line.starts_with(' ') || line.starts_with('\t') { + if is_edp { + if trimmed.starts_with("Modes:") { + continue; + } + if trimmed.contains("px,") && trimmed.contains("Hz") { + let parts: Vec<&str> = trimmed.split(',').collect(); + if parts.len() >= 2 { + let res = parts[0].replace("px", "").trim().to_string(); + let hz_part = parts[1].trim(); + + let is_current = hz_part.contains("(current)"); + let clean_hz_part = hz_part.replace("(current)", "").replace("(preferred)", "").replace("Hz", ""); + let hz_str = clean_hz_part.trim(); + if let Ok(hz) = hz_str.parse::() { + let mode_id = format!("{}@{}", res, hz_str); + let mode_name = format!("{} px, {} Hz", res, hz_str); + if is_current { + current_mode_str = mode_id.clone(); + } + modes.push((mode_id, mode_name, hz)); + } + } + } + } + } else { + let parts: Vec<&str> = trimmed.split_whitespace().collect(); + if let Some(name) = parts.first() { + if name.starts_with("eDP") { + current_output = name.to_string(); + is_edp = true; + } else { + is_edp = false; + } + } + } + } + + if !current_output.is_empty() && !modes.is_empty() { + modes.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)); + if current_mode_str.is_empty() { + current_mode_str = modes.first()?.0.clone(); + } + Some((current_output, current_mode_str, modes)) + } else { + None + } +} + +fn set_wlr_mode(output_name: &str, mode_id: &str) { + let _ = std::process::Command::new("wlr-randr") + .arg("--output") + .arg(output_name) + .arg("--mode") + .arg(mode_id) + .output(); +} From 07e5405677c24ae6e6646e8dd841c3a12b4975cc Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Mon, 29 Jun 2026 12:12:16 +0200 Subject: [PATCH 20/21] Move battery info and charge limit to dedicated page --- rog-control-center/src/ui/mod.rs | 1 + rog-control-center/ui/main_window.slint | 13 ++- rog-control-center/ui/pages/battery.slint | 119 ++++++++++++++++++++++ rog-control-center/ui/pages/system.slint | 101 ------------------ 4 files changed, 131 insertions(+), 103 deletions(-) create mode 100644 rog-control-center/ui/pages/battery.slint diff --git a/rog-control-center/src/ui/mod.rs b/rog-control-center/src/ui/mod.rs index aac4da7f..3c1e4f23 100644 --- a/rog-control-center/src/ui/mod.rs +++ b/rog-control-center/src/ui/mod.rs @@ -147,6 +147,7 @@ pub fn setup_window( available.contains(&"xyz.ljones.Slash".to_string()), available.contains(&"xyz.ljones.FanCurves".to_string()), true, // GPU Configuration + available.contains(&"xyz.ljones.Platform".to_string()), // Battery Info true, // App Settings true, // About ] diff --git a/rog-control-center/ui/main_window.slint b/rog-control-center/ui/main_window.slint index d4e42aa8..d7d42499 100644 --- a/rog-control-center/ui/main_window.slint +++ b/rog-control-center/ui/main_window.slint @@ -9,6 +9,7 @@ import { PageSlash, SlashPageData } from "pages/slash.slint"; import { RogItem } from "widgets/common.slint"; import { PageAura } from "pages/aura.slint"; import { PageGPU, GPUPageData } from "pages/gpu.slint"; +import { PageBattery } from "pages/battery.slint"; import { Node } from "widgets/graph.slint"; export { Node } import { FanPageData, FanType, Profile } from "types/fan_types.slint"; @@ -33,6 +34,7 @@ export component MainWindow inherits Window { true, true, true, // GPU Configuration + true, // Battery Info true, // App Settings true, // About ]; @@ -81,6 +83,7 @@ export component MainWindow inherits Window { @tr("Menu4" => "Slash Lighting"), @tr("Menu5" => "Fan Curves"), @tr("Menu6" => "GPU Configuration"), + @tr("Menu9" => "Battery Info"), @tr("Menu7" => "App Settings"), @tr("Menu8" => "About"), ]; @@ -135,18 +138,24 @@ export component MainWindow inherits Window { visible: side-bar.current-item == 5; } - if(side-bar.current-item == 6): PageAppSettings { + if(side-bar.current-item == 6): PageBattery { width: root.width - side-bar.width; height: root.height + 12px; visible: side-bar.current-item == 6; } - if(side-bar.current-item == 7): PageAbout { + if(side-bar.current-item == 7): PageAppSettings { width: root.width - side-bar.width; height: root.height + 12px; visible: side-bar.current-item == 7; } + if(side-bar.current-item == 8): PageAbout { + width: root.width - side-bar.width; + height: root.height + 12px; + visible: side-bar.current-item == 8; + } + if toast: Rectangle { x: 0px; y: root.height - self.height; diff --git a/rog-control-center/ui/pages/battery.slint b/rog-control-center/ui/pages/battery.slint new file mode 100644 index 00000000..6452a4a0 --- /dev/null +++ b/rog-control-center/ui/pages/battery.slint @@ -0,0 +1,119 @@ +import { SystemSlider, RogItem } from "../widgets/common.slint"; +import { Palette, VerticalBox, ScrollView } from "std-widgets.slint"; +import { SystemPageData } from "system.slint"; + +export component PageBattery inherits Rectangle { + clip: true; + + ScrollView { + VerticalLayout { + padding: 12px; + spacing: 8px; + alignment: LayoutAlignment.start; + + Rectangle { + background: Palette.alternate-background; + border-color: Palette.border; + border-width: 1px; + border-radius: 2px; + height: 36px; + Text { + font-size: 16px; + color: Palette.control-foreground; + horizontal-alignment: TextHorizontalAlignment.center; + text: @tr("Battery Information"); + } + } + + if SystemPageData.charge_control_end_threshold != -1: SystemSlider { + text: @tr("Charge limit"); + minimum: 20; + maximum: 100; + has_reset: false; + enabled <=> SystemPageData.charge_control_enabled; + value: SystemPageData.charge_control_end_threshold; + released => { + SystemPageData.charge_control_end_threshold = self.value; + SystemPageData.cb_charge_control_end_threshold(Math.round(SystemPageData.charge_control_end_threshold)) + } + } + + if SystemPageData.battery_health != -1: Rectangle { + background: Palette.alternate-background; + border-color: Palette.border; + border-width: 1px; + border-radius: 4px; + + VerticalLayout { + padding: 12px; + spacing: 8px; + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Battery Health (Capacity):"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_health + "%"; + font-weight: 700; + color: SystemPageData.battery_health > 80 ? #22c55e : (SystemPageData.battery_health > 50 ? #eab308 : #ef4444); + } + } + + if SystemPageData.battery_cycle_count != -1: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Cycle Count:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_cycle_count; + font-weight: 700; + color: Palette.control-foreground; + } + } + + HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Status:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_status; + font-weight: 700; + color: Palette.control-foreground; + } + } + + if SystemPageData.battery_power_consumption != -1.0: HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: @tr("Power Draw:"); + color: Palette.control-foreground; + } + Text { + text: Math.round(SystemPageData.battery_power_consumption * 10) / 10 + " W"; + font-weight: 700; + color: SystemPageData.battery_status == "Discharging" ? #ef4444 : #22c55e; + } + } + + if SystemPageData.battery_time_estimate != "": HorizontalLayout { + alignment: LayoutAlignment.space-between; + Text { + text: SystemPageData.battery_status == "Charging" ? @tr("Time to Full:") : @tr("Time Remaining:"); + color: Palette.control-foreground; + } + Text { + text: SystemPageData.battery_time_estimate; + font-weight: 700; + color: Palette.control-foreground; + } + } + } + } + } + } +} diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index a6f6a2d3..facef816 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -211,107 +211,6 @@ export component PageSystem inherits Rectangle { } } - if SystemPageData.charge_control_end_threshold != -1: SystemSlider { - text: @tr("Charge limit"); - minimum: 20; - maximum: 100; - has_reset: false; - enabled <=> SystemPageData.charge_control_enabled; - value: SystemPageData.charge_control_end_threshold; - released => { - SystemPageData.charge_control_end_threshold = self.value; - SystemPageData.cb_charge_control_end_threshold(Math.round(SystemPageData.charge_control_end_threshold)) - } - } - - if SystemPageData.battery_health != -1: Rectangle { - background: Palette.alternate-background; - border-color: Palette.border; - border-width: 1px; - border-radius: 4px; - - VerticalLayout { - padding: 12px; - spacing: 8px; - - HorizontalLayout { - alignment: LayoutAlignment.start; - spacing: 8px; - Text { - font-size: 14px; - font-weight: 700; - color: Palette.accent-background; - text: @tr("Battery Information"); - } - } - - HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Battery Health (Capacity):"); - color: Palette.control-foreground; - } - Text { - text: SystemPageData.battery_health + "%"; - font-weight: 700; - color: SystemPageData.battery_health > 80 ? #22c55e : (SystemPageData.battery_health > 50 ? #eab308 : #ef4444); - } - } - - if SystemPageData.battery_cycle_count != -1: HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Cycle Count:"); - color: Palette.control-foreground; - } - Text { - text: SystemPageData.battery_cycle_count; - font-weight: 700; - color: Palette.control-foreground; - } - } - - HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Status:"); - color: Palette.control-foreground; - } - Text { - text: SystemPageData.battery_status; - font-weight: 700; - color: Palette.control-foreground; - } - } - - if SystemPageData.battery_power_consumption != -1.0: HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Power Draw:"); - color: Palette.control-foreground; - } - Text { - text: Math.round(SystemPageData.battery_power_consumption * 10) / 10 + " W"; - font-weight: 700; - color: SystemPageData.battery_status == "Discharging" ? #ef4444 : #22c55e; - } - } - - if SystemPageData.battery_time_estimate != "": HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: SystemPageData.battery_status == "Charging" ? @tr("Time to Full:") : @tr("Time Remaining:"); - color: Palette.control-foreground; - } - Text { - text: SystemPageData.battery_time_estimate; - font-weight: 700; - color: Palette.control-foreground; - } - } - } - } - if SystemPageData.refresh_rate_active_idx != -1: Rectangle { background: Palette.alternate-background; border-color: Palette.border; From 22b211a452f6cefc9e9b29d6140c56f6abeebe53 Mon Sep 17 00:00:00 2001 From: Marco Scardovi Date: Wed, 8 Jul 2026 09:02:38 +0200 Subject: [PATCH 21/21] ui: remove battery cycle count display from control center --- rog-control-center/src/ui/mod.rs | 6 +- rog-control-center/src/ui/setup_system.rs | 116 +++++++++++++--------- rog-control-center/ui/pages/battery.slint | 15 +-- rog-control-center/ui/pages/system.slint | 2 +- 4 files changed, 72 insertions(+), 67 deletions(-) diff --git a/rog-control-center/src/ui/mod.rs b/rog-control-center/src/ui/mod.rs index 3c1e4f23..97cb0b15 100644 --- a/rog-control-center/src/ui/mod.rs +++ b/rog-control-center/src/ui/mod.rs @@ -146,10 +146,10 @@ pub fn setup_window( available.contains(&"xyz.ljones.Anime".to_string()), available.contains(&"xyz.ljones.Slash".to_string()), available.contains(&"xyz.ljones.FanCurves".to_string()), - true, // GPU Configuration + true, // GPU Configuration available.contains(&"xyz.ljones.Platform".to_string()), // Battery Info - true, // App Settings - true, // About + true, // App Settings + true, // About ] .into(), ); diff --git a/rog-control-center/src/ui/setup_system.rs b/rog-control-center/src/ui/setup_system.rs index eb2ad576..d7926010 100644 --- a/rog-control-center/src/ui/setup_system.rs +++ b/rog-control-center/src/ui/setup_system.rs @@ -38,7 +38,6 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { ui.global::() .set_charge_control_enabled(false); ui.global::().set_battery_health(-1); - ui.global::().set_battery_cycle_count(-1); ui.global::() .set_battery_power_consumption(-1.0); ui.global::() @@ -80,16 +79,19 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { let backend = detect_display_backend(); let handle_copy = ui.as_weak(); - ui.global::().on_cb_refresh_rate_changed(move |idx| { - if let Some((name, _, modes)) = get_display_info(backend) { - if let Some(mode) = modes.get(idx as usize) { - set_display_mode(backend, &name, &mode.0); - if let Some(handle) = handle_copy.upgrade() { - handle.global::().set_refresh_rate_active_idx(idx); + ui.global::() + .on_cb_refresh_rate_changed(move |idx| { + if let Some((name, _, modes)) = get_display_info(backend) { + if let Some(mode) = modes.get(idx as usize) { + set_display_mode(backend, &name, &mode.0); + if let Some(handle) = handle_copy.upgrade() { + handle + .global::() + .set_refresh_rate_active_idx(idx); + } } } - } - }); + }); let handle = ui.as_weak(); let config_clone = config.clone(); @@ -98,32 +100,30 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { let mut prev_online = None; loop { let power = rog_platform::power::AsusPower::new().ok(); - let (has_bat, health, cycles, consumption, status, estimate_str) = - if let Some(ref p) = power { - if p.has_battery() { - let health = p.get_battery_health().unwrap_or(0) as i32; - let cycles = p.get_battery_cycle_count().unwrap_or(-1); - let consumption = p.get_battery_power_consumption().unwrap_or(-1.0); - let status = p - .get_battery_status() - .unwrap_or_else(|_| "Unknown".to_string()); - let estimate = p.get_battery_time_estimate().ok().flatten(); - let est_str = if let Some((_, h, m)) = estimate { - if h > 0 { - format!("{}h {}m", h, m) - } else { - format!("{}m", m) - } + let (has_bat, health, consumption, status, estimate_str) = if let Some(ref p) = power { + if p.has_battery() { + let health = p.get_battery_health().unwrap_or(0) as i32; + let consumption = p.get_battery_power_consumption().unwrap_or(-1.0); + let status = p + .get_battery_status() + .unwrap_or_else(|_| "Unknown".to_string()); + let estimate = p.get_battery_time_estimate().ok().flatten(); + let est_str = if let Some((_, h, m)) = estimate { + if h > 0 { + format!("{}h {}m", h, m) } else { - "".to_string() - }; - (true, health, cycles, consumption, status, est_str) + format!("{}m", m) + } } else { - (false, -1, -1, -1.0, "Unknown".to_string(), "".to_string()) - } + "".to_string() + }; + (true, health, consumption, status, est_str) } else { - (false, -1, -1, -1.0, "Unknown".to_string(), "".to_string()) - }; + (false, -1, -1.0, "Unknown".to_string(), "".to_string()) + } + } else { + (false, -1, -1.0, "Unknown".to_string(), "".to_string()) + }; let cpu_temp = get_cpu_temp(); let gpu_temp = get_gpu_temp(); @@ -165,7 +165,10 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { } } - let curr_online = power.as_ref().map(|p| p.get_online().unwrap_or(1) == 1).unwrap_or(true); + let curr_online = power + .as_ref() + .map(|p| p.get_online().unwrap_or(1) == 1) + .unwrap_or(true); if let Some(prev) = prev_online { if prev != curr_online { let auto_switch_enabled = if let Ok(lock) = config_clone.try_lock() { @@ -183,7 +186,9 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { if mode.0 != current_mode_id { set_display_mode(backend, &output_name, &mode.0); current_mode_id = mode.0.clone(); - if let Some(pos) = available_modes_store.iter().position(|x| x.0 == mode.0) { + if let Some(pos) = + available_modes_store.iter().position(|x| x.0 == mode.0) + { active_idx = pos as i32; } let msg = if curr_online { @@ -210,7 +215,6 @@ pub fn setup_system_page(ui: &MainWindow, config: Arc>) { let data = ui.global::(); if has_bat { data.set_battery_health(health); - data.set_battery_cycle_count(cycles); data.set_battery_power_consumption(consumption); data.set_battery_status(status.into()); data.set_battery_time_estimate(estimate_str.into()); @@ -1119,16 +1123,26 @@ enum WaylandDisplayBackend { } fn detect_display_backend() -> WaylandDisplayBackend { - if std::process::Command::new("kscreen-doctor").arg("--help").output().is_ok() { + if std::process::Command::new("kscreen-doctor") + .arg("--help") + .output() + .is_ok() + { WaylandDisplayBackend::KScreenDoctor - } else if std::process::Command::new("wlr-randr").arg("--help").output().is_ok() { + } else if std::process::Command::new("wlr-randr") + .arg("--help") + .output() + .is_ok() + { WaylandDisplayBackend::WlrRandr } else { WaylandDisplayBackend::None } } -fn get_display_info(backend: WaylandDisplayBackend) -> Option<(String, String, Vec<(String, String, f64)>)> { +fn get_display_info( + backend: WaylandDisplayBackend, +) -> Option<(String, String, Vec<(String, String, f64)>)> { match backend { WaylandDisplayBackend::KScreenDoctor => get_kscreen_info(), WaylandDisplayBackend::WlrRandr => get_wlr_info(), @@ -1176,10 +1190,13 @@ fn get_kscreen_info() -> Option<(String, String, Vec<(String, String, f64)>)> { return None; } let config: KScreenConfig = serde_json::from_slice(&output.stdout).ok()?; - let panel = config.outputs.iter().find(|o| o.output_type == 7 || o.name.starts_with("eDP"))?; + let panel = config + .outputs + .iter() + .find(|o| o.output_type == 7 || o.name.starts_with("eDP"))?; let current_mode = panel.modes.iter().find(|m| m.id == panel.current_mode_id)?; let current_res = current_mode.name.split('@').next()?.trim(); - + let mut available_rates = Vec::new(); for m in &panel.modes { if let Some(res) = m.name.split('@').next() { @@ -1189,7 +1206,7 @@ fn get_kscreen_info() -> Option<(String, String, Vec<(String, String, f64)>)> { } } available_rates.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)); - + Some((panel.name.clone(), current_mode.id.clone(), available_rates)) } @@ -1201,19 +1218,17 @@ fn set_kscreen_mode(output_name: &str, mode_id: &str) { } fn get_wlr_info() -> Option<(String, String, Vec<(String, String, f64)>)> { - let output = std::process::Command::new("wlr-randr") - .output() - .ok()?; + let output = std::process::Command::new("wlr-randr").output().ok()?; if !output.status.success() { return None; } let stdout = String::from_utf8_lossy(&output.stdout); - + let mut current_output = String::new(); let mut is_edp = false; let mut current_mode_str = String::new(); let mut modes = Vec::new(); - + for line in stdout.lines() { let trimmed = line.trim(); if line.starts_with(' ') || line.starts_with('\t') { @@ -1226,9 +1241,12 @@ fn get_wlr_info() -> Option<(String, String, Vec<(String, String, f64)>)> { if parts.len() >= 2 { let res = parts[0].replace("px", "").trim().to_string(); let hz_part = parts[1].trim(); - + let is_current = hz_part.contains("(current)"); - let clean_hz_part = hz_part.replace("(current)", "").replace("(preferred)", "").replace("Hz", ""); + let clean_hz_part = hz_part + .replace("(current)", "") + .replace("(preferred)", "") + .replace("Hz", ""); let hz_str = clean_hz_part.trim(); if let Ok(hz) = hz_str.parse::() { let mode_id = format!("{}@{}", res, hz_str); @@ -1253,7 +1271,7 @@ fn get_wlr_info() -> Option<(String, String, Vec<(String, String, f64)>)> { } } } - + if !current_output.is_empty() && !modes.is_empty() { modes.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal)); if current_mode_str.is_empty() { diff --git a/rog-control-center/ui/pages/battery.slint b/rog-control-center/ui/pages/battery.slint index 6452a4a0..0a8d4a76 100644 --- a/rog-control-center/ui/pages/battery.slint +++ b/rog-control-center/ui/pages/battery.slint @@ -60,20 +60,7 @@ export component PageBattery inherits Rectangle { color: SystemPageData.battery_health > 80 ? #22c55e : (SystemPageData.battery_health > 50 ? #eab308 : #ef4444); } } - - if SystemPageData.battery_cycle_count != -1: HorizontalLayout { - alignment: LayoutAlignment.space-between; - Text { - text: @tr("Cycle Count:"); - color: Palette.control-foreground; - } - Text { - text: SystemPageData.battery_cycle_count; - font-weight: 700; - color: Palette.control-foreground; - } - } - + HorizontalLayout { alignment: LayoutAlignment.space-between; Text { diff --git a/rog-control-center/ui/pages/system.slint b/rog-control-center/ui/pages/system.slint index facef816..d62ae983 100644 --- a/rog-control-center/ui/pages/system.slint +++ b/rog-control-center/ui/pages/system.slint @@ -153,7 +153,7 @@ export global SystemPageData { in-out property ppt_enabled; callback cb_ppt_enabled(bool); in-out property battery_health: -1; - in-out property battery_cycle_count: -1; + in-out property battery_power_consumption: -1.0; in-out property battery_status: "Unknown"; in-out property battery_time_estimate: "";