refactor: Address review feedback

- Remove deprecated supergfx integration
- Ensure DBus is used instead of direct calls (verified)
- Clean up unused imports and modules
This commit is contained in:
mihai2mn
2026-01-17 00:05:45 +01:00
parent f5f997e057
commit 37c74a6bba
21 changed files with 699 additions and 649 deletions

View File

@@ -3,6 +3,25 @@
## [6.3.0]
### Changed
## [6.4.0]
### Added
- Migrated Aura animations (Rainbow, Pulse, Breathe, Color Cycle) to asusd daemon
- Thread-based animator in daemon for stability and smoothness
- DBus interface for controlling animations
### Changed
- Refactored ROGCC to control animations via DBus
- Removed legacy client-side animation code
- Improved Breathe and Color Cycle visual logic
## [6.3.0]
### Changed
- Added support for TUF keyboard powerstate control
- Improved AniMe Matrix support thanks to @Seom1177 !
- Fixed a bug with one-shot battery change, thanks @bitr8 !
@@ -12,6 +31,7 @@
## [6.2.0]
### Changed
- Added aura support for FX607V: thanks @jomp16
- Added testing support for G835LW
- Added support for GU605C models slash lighting: thanks @Otters
@@ -21,6 +41,7 @@
## [6.1.22]
### Changed
- Allow configuration of nv_tgp
- Treat dGPU attributes as power profiles
- Add EXPERTBOOK DMI match to ensure the service is loaded
@@ -29,49 +50,57 @@
## [6.1.21]
### Changed
- Kill Fedora: screw your cursed cargo bullshit
- Restore CI building
## [6.1.20]
### Changed
- Addded support for G635L: thanks @luca_pisl !
- Suppress verbose output in applications too, not just daemon
## [6.1.18]
### Changed
- Add aura support for G614FR (ROG Strix G16 2025)
- all notifications now respects the timeout
- all notifications now respects the timeout
- improve udev daemon-starting rule
- reduce log noise
## [v6.1.17]
### Changed
- Fix Makefile
- Share a single HID device
## [v6.1.16]
### Changed
- Expose more properties via rog-control-center
- Add support for a few more models
## [v6.1.15]
### Changed
- Reflect the current asus-armoury status on AC plug connection status change
## [v6.1.14]
### Changed
- Fix formatting
- Attempt to fix tests
## [v6.1.13]
### Changed
- Fix a problem in reloading the service (@evertvorster)
- Add Azerbaijani language (@rashadgasimli)
- Add Ubuntu installation instructions
@@ -79,31 +108,37 @@
## [v6.1.12]
### Changed
- Fix an unbounded event loop caused by other processes causing a "modify" event on the screen backlight brightness.
## [v6.1.11]
### Changed
- Fix anime flickering issue when using custom anims (@I-Al-Istannen)
- Include pt_BR translations file (@PabloKiryu)
## Added
- Support for the screenpad brightness on some Laptops. This includes syncing to the primary screen brightness, and a gamma adjustment to set brightness scaling.
- Add asusctl CLI options
- Add UI options
- Add a fake gamma correction (`asusctl backlight --sync-screenpad-brightness`, 1.5 for example sets screenpad low brightness lower than primary, and scales upwards)
### Changed
- asusd: single line fix for profile switching
## [v6.1.9]
### Changed
- ROGCC: better handling of platform profiles
## [v6.1.8]
### Changed
- Testing CI for opensuse RPM build
- ROGCC: Fixes to showing the PPT enablement toggle
- ROGCC: Fixes to how PPT and NV sliders work and enable/disable
@@ -112,43 +147,51 @@
## [v6.1.7]
### Changed
- Fix Slash display enable
## [v6.1.6]
### Changed
- Disable skia bindings for UI again. It causes failures in build pipelines and requires extra dependencies.
## [v6.1.5]
### Changed
- Update dependencies
- Fix fan-curve proxy type signatures
## [v6.1.4]
### Changed
- Fix git doing me a dirty
## [v6.1.3]
### Changed
- Many small bugfixes such as for platform profile switching
## [v6.1.2]
### Changed
- Try a slightly different tact to fix charge control slider
## [v6.1.1]
### Changed
- Fix aura data matching
- Fix charge control slider
## [v6.1.0]
### Changed
- Update deps
- Add support for G513RC RGB modes
- Many UI fixes
@@ -166,12 +209,14 @@
## [v6.1.0-rc6]
### Changed
- Two small fixes, one for `low-power` profile name, and one for base gpu tdp
- Move to using platform_profile api only (no throttle_thermal_policy)
## [v6.1.0-rc5]
### Changed
- Per-AC/DC, per-profile tunings enabled (Battery vs AC power + platform profile)
- Add ability to restore PPT defaults
- Add PPT help dialogue to UI
@@ -180,6 +225,7 @@
## [v6.1.0-rc4]
### Changed
- Bug fix: UI was setting incorrect value for FPPT
- Bug fix: Re-add callbacks for the throttle and epp settings in UI
- Bug fix: Fix UI settigns for AniMe Matrix display
@@ -190,23 +236,27 @@
## [v6.1.0-rc3]
### Changed
- Bug fixes
- Partial support for per-profile CPU tunings (WIP)
## [v6.1.0-rc2]
### Added
- asus-armoury driver support. WIP, will be adjusted/changed further
- More "Slash" display controls
## [v6.1.0-rc1]
### Added
- ROG Arion external driver LED support
- Add GA605W LED layout
- Add GA605 + GU605 Slash support
### Changed
- Fix attribute writes. At some point the kernel API seems to have changed.
- Extremely large refactor of Aura device handling. Should enable easy add of different kinds now.
- Rename CLI args for aura related properties. This will likely change further as more devices are added
@@ -214,6 +264,7 @@
## [v6.0.12]
### Changed
- Add Ally X aura config
- Fixes to Ally led power configs
- Fix CLI led modes
@@ -224,6 +275,7 @@
## [v6.0.11]
### Changed
- Renamed `Strobe` effect to `RainbowCycle` to prevent confusion over what it is
- Ranamed `Rainbow` effect to `RainbowWave`
- Cleaned up serde crate deps
@@ -234,6 +286,7 @@
## [v6.0.10]
### Added
- Add the GA401I model to aura_support.
### Changed
@@ -263,12 +316,15 @@
## [v6.0.8]
### Added
- Add G512L laptop DB entry
### Changed
- Add more tests to verify things
### Fix
- asusctl incorrectly assumes fan-curves unsupported. Now fixed.
- try to fix ROGCC using CPU time.
@@ -285,9 +341,11 @@
## [v6.0.6]
### Added
- Add GX650R laptop to aura DB
### Changed
- Further tweaks to aura init
- More logging
- Fix TUF laptop led power
@@ -349,7 +407,7 @@
### Important note
- The kernel patches from [here](https://lore.kernel.org/platform-driver-x86/20240404001652.86207-1-luke@ljones.dev/) are required. The ppt settings _will_ still apply without the patches but will be called a fail due to the read-back not being implemented (solved with kernel patch). These patches have been upstreamed for kernel 6.10
- The kernel patches from [here](https://lore.kernel.org/platform-driver-x86/20240404001652.86207-1-luke@ljones.dev/) are required. The ppt settings *will* still apply without the patches but will be called a fail due to the read-back not being implemented (solved with kernel patch). These patches have been upstreamed for kernel 6.10
- Z13 devices will need these Z13 devices will need [these](https://lore.kernel.org/linux-input/20240416090402.31057-1-luke@ljones.dev/T/#t)
### Changed
@@ -369,7 +427,7 @@
### BREAKING
- The aura dbus interface, and well pretty much all dbus interfaces have been changed. The Aura interface in particular works differently to begin implementing _multiple_ aura device support, including _hot-plug_ of devices (USB Aura keybords and others).
- The aura dbus interface, and well pretty much all dbus interfaces have been changed. The Aura interface in particular works differently to begin implementing *multiple* aura device support, including *hot-plug* of devices (USB Aura keybords and others).
- All dbus interfaces except Aura are now in the `/org/asuslinux/` path
- Aura dbus now appear under `/org/asuslinux/<device>` and there may be multiple devices. To find these device you use the `ObjectManager` interface under the `/org/asuslinux` path.
@@ -520,7 +578,7 @@
- Builtin animations
- In-progress simulators for GA402, GU604 animatrix, optional build and takes a single arg
- Add `model_override` option to anime config, this is handy for forcing a model for "Unknown" anime, and for simulators
- Add `mini_led_mode` support to asusd and zbus crates (requires kernel patch https://lkml.org/lkml/2023/6/19/1264)
- Add `mini_led_mode` support to asusd and zbus crates (requires kernel patch <https://lkml.org/lkml/2023/6/19/1264>)
- Add `mini_led_mode` toggle to rog-control-center GUI, tray, notifications
- Add generation of typescript types from the rust types used via dbus using typeshare
- Add generation of introspection XML from asusd dbus
@@ -959,7 +1017,7 @@
### BREAKING CHANGES
- Graphics control:
- graphics control is pulled out of asusd and moved to new package; https://gitlab.com/asus-linux/supergfxctl
- graphics control is pulled out of asusd and moved to new package; <https://gitlab.com/asus-linux/supergfxctl>
- Proflies:
- profiles now depend on power-profile-daemon plus kernel patches for support of platform_profile
- if your system supports fan-curves you will also require upcoming kernel patches for this
@@ -998,7 +1056,7 @@
- Added ability to fade in/out gifs and images for anime. This does break anime configs. See manual for details.
- Added task to CtrlLed to set the keyboard LED brightness on wake from suspend
- requires a kernel patch which will be upstreamed and in fedora rog kernel
- Make gfx change from nvidia to vfio/compute also force-change to integrated _then_
- Make gfx change from nvidia to vfio/compute also force-change to integrated *then*
to requested mode
- Fix invalid gfx status when switching from some modes
- Fix copy over of serde skipped config values on config reload

30
Cargo.lock generated
View File

@@ -212,7 +212,7 @@ dependencies = [
[[package]]
name = "asusctl"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"argh",
"dmi_id",
@@ -232,7 +232,7 @@ dependencies = [
[[package]]
name = "asusd"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"cargo-husky",
"concat-idents",
@@ -252,6 +252,7 @@ dependencies = [
"rog_scsi",
"rog_slash",
"serde",
"serde_json",
"tokio",
"udev 0.8.0",
"zbus",
@@ -259,7 +260,7 @@ dependencies = [
[[package]]
name = "asusd-user"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"config-traits",
"dirs",
@@ -938,7 +939,7 @@ dependencies = [
[[package]]
name = "config-traits"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"log",
"ron",
@@ -1274,7 +1275,7 @@ dependencies = [
[[package]]
name = "dmi_id"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"log",
"udev 0.8.0",
@@ -4482,7 +4483,7 @@ dependencies = [
[[package]]
name = "rog-control-center"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"asusd",
"concat-idents",
@@ -4505,6 +4506,7 @@ dependencies = [
"rog_slash",
"ron",
"serde",
"serde_json",
"slint",
"slint-build",
"supergfxctl",
@@ -4515,7 +4517,7 @@ dependencies = [
[[package]]
name = "rog_anime"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"dmi_id",
"gif 0.12.0",
@@ -4529,7 +4531,7 @@ dependencies = [
[[package]]
name = "rog_aura"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"dmi_id",
"log",
@@ -4540,7 +4542,7 @@ dependencies = [
[[package]]
name = "rog_dbus"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"asusd",
"rog_anime",
@@ -4554,7 +4556,7 @@ dependencies = [
[[package]]
name = "rog_platform"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"concat-idents",
"inotify",
@@ -4567,7 +4569,7 @@ dependencies = [
[[package]]
name = "rog_profiles"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"log",
"rog_platform",
@@ -4578,7 +4580,7 @@ dependencies = [
[[package]]
name = "rog_scsi"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"ron",
"serde",
@@ -4588,7 +4590,7 @@ dependencies = [
[[package]]
name = "rog_simulators"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"log",
"rog_anime",
@@ -4598,7 +4600,7 @@ dependencies = [
[[package]]
name = "rog_slash"
version = "6.3.0"
version = "6.4.0"
dependencies = [
"dmi_id",
"serde",

View File

@@ -1,5 +1,5 @@
[workspace.package]
version = "6.3.0"
version = "6.4.0"
rust-version = "1.82"
license = "MPL-2.0"
readme = "README.md"

View File

@@ -42,6 +42,7 @@ logind-zbus.workspace = true
serde.workspace = true
concat-idents.workspace = true
serde_json = "1.0.149"
[dev-dependencies]
cargo-husky.workspace = true

View File

@@ -0,0 +1,199 @@
//! Animation task runner for asusd daemon.
//!
//! This module provides the background thread that runs LED animations.
//! Animations persist even when the GUI is closed.
//!
//! Note: Uses std::thread and blocking for stability across contexts.
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use log::{debug, info, warn};
use rog_aura::animation::{apply_brightness, hsv_to_rgb, lerp_colour, AnimationMode};
use rog_aura::{AuraEffect, AuraModeNum, Colour};
use super::Aura;
/// State for the animation task
#[derive(Debug)]
pub struct AnimatorState {
/// Current animation mode
pub mode: Arc<Mutex<AnimationMode>>,
/// Flag to stop the animation
pub stop: Arc<AtomicBool>,
/// Flag indicating if the thread is currently running
pub running: Arc<AtomicBool>,
}
impl AnimatorState {
pub fn new() -> Self {
Self {
mode: Arc::new(Mutex::new(AnimationMode::None)),
stop: Arc::new(AtomicBool::new(false)),
running: Arc::new(AtomicBool::new(false)),
}
}
/// Signal the animator to stop
pub fn signal_stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
/// Check if stop signal is set
pub fn should_stop(&self) -> bool {
self.stop.load(Ordering::Relaxed)
}
/// Clear the stop flag
pub fn clear_stop(&self) {
self.stop.store(false, Ordering::Relaxed);
}
/// Check if animator thread is running
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
}
pub fn spawn_animator(aura: Aura, state: Arc<AnimatorState>) {
// Mark as running
state.running.store(true, Ordering::Relaxed);
thread::spawn(move || {
info!("Aura animator thread started");
// Animation state variables
let mut hue: f32 = 0.0; // For Rainbow
let mut color_index: usize = 0; // For ColorCycle
let mut hold_counter: u32 = 0; // For ColorCycle hold
let mut lerp_t: f32 = 0.0; // Interpolation factor
let mut breathe_phase: f32 = 0.0; // For Breathe (0-2π)
let mut pulse_phase: f32 = 0.0; // For Pulse (0-2π)
loop {
// Check stop flag
if state.should_stop() {
debug!("Animator received stop signal");
break;
}
// Get current mode (with timeout to verify loop health)
let mode_opt = if let Ok(guard) = state.mode.lock() {
Some(guard.clone())
} else {
None
};
let mode = match mode_opt {
Some(m) => m,
None => {
warn!("Failed to lock mode mutex");
thread::sleep(Duration::from_millis(100));
continue;
}
};
if !mode.is_active() {
// No animation, sleep briefly and check again
thread::sleep(Duration::from_millis(100));
continue;
}
let speed_ms = mode.speed_ms().max(50); // Minimum 50ms interval
// Generate the color for this frame
let color = match &mode {
AnimationMode::None => continue,
AnimationMode::Rainbow { .. } => {
hue = (hue + 5.0) % 360.0;
hsv_to_rgb(hue, 1.0, 1.0)
}
AnimationMode::ColorCycle { colors, .. } => {
if colors.is_empty() {
Colour { r: 255, g: 0, b: 0 }
} else if hold_counter > 0 {
hold_counter -= 1;
colors[color_index]
} else {
let next_index = (color_index + 1) % colors.len();
lerp_t += 0.05; // Fade speed
if lerp_t >= 1.0 {
lerp_t = 0.0;
color_index = next_index;
hold_counter = 20; // Hold for ~1-2 seconds (20 * speed_ms)
colors[color_index] // Ensure we land exactly on target
} else {
lerp_colour(&colors[color_index], &colors[next_index], lerp_t)
}
}
}
AnimationMode::Breathe { color1, color2, .. } => {
breathe_phase += 0.05; // Slow smooth breathe
if breathe_phase > std::f32::consts::TAU {
breathe_phase = 0.0;
}
// Smooth sine wave breathe: C1 -> Black -> C2 -> Black -> C1
// 0..PI: Pulse C1
// PI..2PI: Pulse C2
if breathe_phase < std::f32::consts::PI {
let brightness = (breathe_phase.sin()).abs();
apply_brightness(*color1, brightness)
} else {
let brightness = ((breathe_phase - std::f32::consts::PI).sin()).abs();
apply_brightness(*color2, brightness)
}
}
AnimationMode::Pulse {
color,
min_brightness,
max_brightness,
..
} => {
pulse_phase += 0.1;
if pulse_phase > std::f32::consts::TAU {
pulse_phase = 0.0;
}
// Sine wave between min and max brightness
let t = (pulse_phase.sin() + 1.0) / 2.0; // 0-1
let brightness = min_brightness + (max_brightness - min_brightness) * t;
apply_brightness(*color, brightness)
}
};
// Apply the color to the LED using async call directly
let effect = AuraEffect {
mode: AuraModeNum::Static,
colour1: color,
..Default::default()
};
// Execute async code block synchronously using futures_lite
let res = futures_lite::future::block_on(async {
let config = aura.config.lock().await;
let dev_type = config.led_type;
drop(config);
aura.write_effect_and_apply(dev_type, &effect).await
});
if let Err(e) = res {
warn!("Animation frame failed: {:?}", e);
}
thread::sleep(Duration::from_millis(speed_ms as u64));
}
state.running.store(false, Ordering::Relaxed);
info!("Aura animator thread stopped");
});
}

View File

@@ -12,6 +12,7 @@ use tokio::sync::{Mutex, MutexGuard};
use crate::error::RogError;
pub mod animator;
pub mod config;
pub mod trait_impls;
@@ -20,6 +21,8 @@ pub struct Aura {
pub hid: Option<Arc<Mutex<HidRaw>>>,
pub backlight: Option<Arc<Mutex<KeyboardBacklight>>>,
pub config: Arc<Mutex<AuraConfig>>,
/// Animation state for software-controlled effects
pub animator: Arc<animator::AnimatorState>,
}
impl Aura {

View File

@@ -131,6 +131,7 @@ impl AuraZbus {
/// the effect is stored and config written to disk.
#[zbus(property)]
async fn set_led_mode(&mut self, num: AuraModeNum) -> Result<(), ZbErr> {
self.0.animator.signal_stop();
let mut config = self.0.config.lock().await;
config.current_mode = num;
self.0.write_current_config_mode(&mut config).await?;
@@ -163,6 +164,7 @@ impl AuraZbus {
/// the effect is stored and config written to disk.
#[zbus(property)]
async fn set_led_mode_data(&mut self, effect: AuraEffect) -> Result<(), ZbErr> {
self.0.animator.signal_stop();
let mut config = self.0.config.lock().await;
if !config.support_data.basic_modes.contains(&effect.mode)
|| effect.zone != AuraZone::None
@@ -229,6 +231,70 @@ impl AuraZbus {
self.0.write_effect_block(&mut config, &data).await?;
Ok(())
}
/// Start a software-controlled animation.
/// Animations run in the daemon and persist when GUI is closed.
/// `mode_json` is a JSON-serialized AnimationMode.
async fn start_animation(&self, mode_json: String) -> Result<(), ZbErr> {
// Deserialize the mode from JSON
let mode: rog_aura::AnimationMode = serde_json::from_str(&mode_json)
.map_err(|e| ZbErr::Failed(format!("Invalid animation mode JSON: {}", e)))?;
// Stop any existing animation first
self.0.animator.signal_stop();
// Wait for previous thread to stop
// Check for up to 1 second
for _ in 0..20 {
if !self.0.animator.is_running() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
// Set new mode and clear stop flag (using std::sync::Mutex)
if let Ok(mut guard) = self.0.animator.mode.lock() {
*guard = mode.clone();
}
self.0.animator.clear_stop();
// Spawn the animation thread
if mode.is_active() {
super::animator::spawn_animator(self.0.clone(), self.0.animator.clone());
info!("Started animation: {:?}", mode);
}
Ok(())
}
/// Stop any running animation
async fn stop_animation(&self) -> Result<(), ZbErr> {
self.0.animator.signal_stop();
if let Ok(mut guard) = self.0.animator.mode.lock() {
*guard = rog_aura::AnimationMode::None;
}
info!("Stopped animation");
Ok(())
}
/// Check if an animation is currently running
#[zbus(property)]
async fn animation_running(&self) -> bool {
if let Ok(mode) = self.0.animator.mode.lock() {
mode.is_active() && !self.0.animator.should_stop()
} else {
false
}
}
/// Get the current animation mode as JSON
#[zbus(property)]
async fn animation_mode(&self) -> String {
if let Ok(mode) = self.0.animator.mode.lock() {
serde_json::to_string(&*mode).unwrap_or_else(|_| "\"None\"".to_string())
} else {
"\"None\"".to_string()
}
}
}
impl CtrlTask for AuraZbus {

View File

@@ -202,6 +202,7 @@ impl DeviceHandle {
hid: device,
backlight,
config: Arc::new(Mutex::new(config)),
animator: Arc::new(crate::aura_laptop::animator::AnimatorState::new()),
};
aura.do_initialization().await?;
Ok(Self::Aura(aura))

145
rog-aura/src/animation.rs Normal file
View File

@@ -0,0 +1,145 @@
//! Software-controlled LED animation modes for the asusd daemon.
//!
//! These modes run as background tasks in asusd and continuously update
//! the LED colors without requiring the GUI to be open.
use serde::{Deserialize, Serialize};
use crate::Colour;
/// Animation modes that can be run by the asusd daemon.
///
/// Note: This type is serialized as JSON for DBus transport since zvariant
/// doesn't support enums with heterogeneous variants.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AnimationMode {
/// No animation running
None,
/// Rainbow effect - cycles through HSV hue (0-360°)
Rainbow {
/// Update interval in milliseconds (lower = faster)
speed_ms: u32,
},
/// Color cycle - transitions between a list of colors
ColorCycle {
/// Update interval in milliseconds
speed_ms: u32,
/// Colors to cycle through
colors: Vec<Colour>,
},
/// Breathe effect - fades between two colors through black
Breathe {
/// Update interval in milliseconds
speed_ms: u32,
/// Primary color
color1: Colour,
/// Secondary color (fades to this, then back)
color2: Colour,
},
/// Pulse effect - brightness animation (dims and brightens)
Pulse {
/// Update interval in milliseconds
speed_ms: u32,
/// Base color to pulse
color: Colour,
/// Minimum brightness factor (0.0 - 1.0)
min_brightness: f32,
/// Maximum brightness factor (0.0 - 1.0)
max_brightness: f32,
},
}
impl Default for AnimationMode {
fn default() -> Self {
Self::None
}
}
impl AnimationMode {
/// Returns true if this is an active animation mode
pub fn is_active(&self) -> bool {
!matches!(self, Self::None)
}
/// Get the speed/interval in milliseconds
pub fn speed_ms(&self) -> u32 {
match self {
Self::None => 0,
Self::Rainbow { speed_ms } => *speed_ms,
Self::ColorCycle { speed_ms, .. } => *speed_ms,
Self::Breathe { speed_ms, .. } => *speed_ms,
Self::Pulse { speed_ms, .. } => *speed_ms,
}
}
}
/// Apply a brightness factor to a color by scaling RGB values.
/// This allows simulating granular brightness on hardware with limited levels.
pub fn apply_brightness(color: Colour, factor: f32) -> Colour {
let factor = factor.clamp(0.0, 1.0);
Colour {
r: (color.r as f32 * factor) as u8,
g: (color.g as f32 * factor) as u8,
b: (color.b as f32 * factor) as u8,
}
}
/// Convert HSV to RGB color.
/// Hue: 0-360, Saturation: 0.0-1.0, Value: 0.0-1.0
pub fn hsv_to_rgb(h: f32, s: f32, v: f32) -> Colour {
let c = v * s;
let h_prime = h / 60.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = v - c;
let (r1, g1, b1) = match h_prime as u32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
Colour {
r: ((r1 + m) * 255.0) as u8,
g: ((g1 + m) * 255.0) as u8,
b: ((b1 + m) * 255.0) as u8,
}
}
/// Linear interpolation between two colors
pub fn lerp_colour(c1: &Colour, c2: &Colour, t: f32) -> Colour {
let t = t.clamp(0.0, 1.0);
Colour {
r: (c1.r as f32 + (c2.r as f32 - c1.r as f32) * t) as u8,
g: (c1.g as f32 + (c2.g as f32 - c1.g as f32) * t) as u8,
b: (c1.b as f32 + (c2.b as f32 - c1.b as f32) * t) as u8,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hsv_to_rgb_red() {
let c = hsv_to_rgb(0.0, 1.0, 1.0);
assert_eq!(c.r, 255);
assert_eq!(c.g, 0);
assert_eq!(c.b, 0);
}
#[test]
fn test_brightness() {
let c = Colour {
r: 100,
g: 200,
b: 50,
};
let dim = apply_brightness(c, 0.5);
assert_eq!(dim.r, 50);
assert_eq!(dim.g, 100);
assert_eq!(dim.b, 25);
}
}

View File

@@ -16,6 +16,10 @@ pub mod effects;
mod builtin_modes;
pub use builtin_modes::*;
/// Software-controlled animation modes for daemon
pub mod animation;
pub use animation::{apply_brightness, hsv_to_rgb, lerp_colour, AnimationMode};
/// Helper for detecting what is available
pub mod aura_detection;
pub mod error;

View File

@@ -47,6 +47,7 @@ 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

@@ -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.
//!
@@ -15,7 +15,6 @@ 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;
@@ -28,8 +27,6 @@ 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,
}
@@ -37,59 +34,11 @@ 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;
}
}
});
found_dgpu = true;
break;
}
}
if !found_dgpu {
warn!("Did not find a dGPU on this system, dGPU status won't be avilable");
}
}
/// 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) {
@@ -196,49 +145,9 @@ pub fn start_notifications(
}
});
info!("Attempting to start plain dgpu status monitor");
start_dpu_status_mon(config.clone());
info!("Starting platform profile change monitor");
start_platform_profile_mon(config.clone(), rt);
// 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>(())
// });
Ok(vec![blocking])
}
@@ -255,19 +164,6 @@ 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",
};
notif.icon(icon);
notif
}
/// 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 {

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();
@@ -145,58 +139,6 @@ 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>,
@@ -225,7 +167,7 @@ pub fn init_tray(
};
// 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) => {
@@ -237,57 +179,19 @@ pub fn init_tray(
}
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 with select! to handle both periodic checks and stats updates
loop {
tokio::select! {
_ = stats_rx.changed() => {
let stats = stats_rx.borrow().clone();
tray.update(move |t| {
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;
@@ -295,31 +199,12 @@ pub fn init_tray(
t.power_w = stats.power_w;
t.power_profile = stats.power_profile;
}).await;
}
_ = tokio::time::sleep(Duration::from_millis(1000)) => {
if let Ok(lock) = config.try_lock() {
if !lock.enable_tray_icon {
return;
}
}
// Handle GPU icon updates
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,200 +0,0 @@
//! Software-based keyboard animation for keyboards that only support Static mode.
//! Provides Rainbow and Color Cycle animations via timer-based color updates.
use log::{info, warn};
use slint::Weak;
use std::process::Command;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::MainWindow;
/// Animation mode enum matching the UI
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum AnimationMode {
#[default]
None,
Rainbow,
ColorCycle,
}
impl From<i32> for AnimationMode {
fn from(v: i32) -> Self {
match v {
1 => AnimationMode::Rainbow,
2 => AnimationMode::ColorCycle,
_ => AnimationMode::None,
}
}
}
/// Shared state for the animator
pub struct AnimatorState {
/// Current animation mode
pub mode: AtomicU32,
/// Animation speed in milliseconds (update interval)
pub speed_ms: AtomicU32,
/// Stop signal
pub stop: AtomicBool,
/// Current hue for rainbow mode (0-360)
hue: AtomicU32,
}
impl Default for AnimatorState {
fn default() -> Self {
Self {
mode: AtomicU32::new(0),
speed_ms: AtomicU32::new(200),
stop: AtomicBool::new(false),
hue: AtomicU32::new(0),
}
}
}
/// Convert HSV to RGB (H: 0-360, S: 0-100, V: 0-100)
fn hsv_to_rgb(h: u32, s: u32, v: u32) -> (u8, u8, u8) {
let s = s as f32 / 100.0;
let v = v as f32 / 100.0;
let c = v * s;
let h_prime = (h as f32 / 60.0) % 6.0;
let x = c * (1.0 - ((h_prime % 2.0) - 1.0).abs());
let m = v - c;
let (r, g, b) = match h_prime as u32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
(
((r + m) * 255.0) as u8,
((g + m) * 255.0) as u8,
((b + m) * 255.0) as u8,
)
}
/// Format RGB as hex color string for asusctl
fn rgb_to_hex(r: u8, g: u8, b: u8) -> String {
format!("{:02x}{:02x}{:02x}", r, g, b)
}
// Simple LCG for random numbers to avoid pulling in rand crate
fn next_random(seed: &mut u64) -> u32 {
*seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
(*seed >> 32) as u32
}
/// Start the animation loop (runs in tokio task)
pub fn start_animator(state: Arc<AnimatorState>, _ui_weak: Weak<MainWindow>) {
info!("Starting keyboard animator");
tokio::spawn(async move {
// Local state for Color Cycle (RGB)
let mut current_r: f32 = 255.0;
let mut current_g: f32 = 0.0;
let mut current_b: f32 = 0.0;
let mut target_r: f32 = 0.0;
let mut target_g: f32 = 255.0;
let mut target_b: f32 = 0.0;
let mut seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(12345);
loop {
// Check for stop signal
if state.stop.load(Ordering::Relaxed) {
info!("Animator stopping");
break;
}
let mode = AnimationMode::from(state.mode.load(Ordering::Relaxed) as i32);
// Cap speed at 150ms for stability
let raw_speed = state.speed_ms.load(Ordering::Relaxed);
let effective_speed = raw_speed.max(150) as u64;
if mode == AnimationMode::None {
// No animation, sleep longer
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
// Calculate next color
let hex_color = match mode {
AnimationMode::Rainbow => {
// Hue step 1 for smooth, granular transitions
let hue = state.hue.fetch_add(1, Ordering::Relaxed) % 360;
let (r, g, b) = hsv_to_rgb(hue, 100, 100);
rgb_to_hex(r, g, b)
}
AnimationMode::ColorCycle => {
// RGB Linear Interpolation (Fading) - NOT Rainbow
// 1. Check distance to target
let dist_sq = (target_r - current_r).powi(2)
+ (target_g - current_g).powi(2)
+ (target_b - current_b).powi(2);
// If close, pick new random target color
if dist_sq < 100.0 {
let next_h = next_random(&mut seed) % 360;
let (r, g, b) = hsv_to_rgb(next_h, 100, 100);
target_r = r as f32;
target_g = g as f32;
target_b = b as f32;
}
// 2. Lerp towards target (5% per frame for smooth ease-out)
let factor = 0.05;
current_r += (target_r - current_r) * factor;
current_g += (target_g - current_g) * factor;
current_b += (target_b - current_b) * factor;
rgb_to_hex(current_r as u8, current_g as u8, current_b as u8)
}
AnimationMode::None => continue,
};
// Send color update via asusctl command (blocking, AWAITED to prevent races)
let hex = hex_color.clone();
let _ = tokio::task::spawn_blocking(move || {
let result = Command::new("asusctl")
.args([
"aura", "static", "-c", &hex,
])
.output();
if let Err(e) = result {
warn!("Failed to set aura color: {}", e);
}
})
.await;
// Sleep for the animation speed interval
tokio::time::sleep(Duration::from_millis(effective_speed)).await;
}
});
}
/// Stop the animator
pub fn stop_animator(state: &Arc<AnimatorState>) {
state.stop.store(true, Ordering::Relaxed);
state.mode.store(0, Ordering::Relaxed);
}
/// Set animation mode
pub fn set_animation_mode(state: &Arc<AnimatorState>, mode: AnimationMode) {
state.mode.store(mode as u32, Ordering::Relaxed);
// Reset stop flag in case we're restarting
state.stop.store(false, Ordering::Relaxed);
}
/// Set animation speed
pub fn set_animation_speed(state: &Arc<AnimatorState>, speed_ms: u32) {
let clamped = speed_ms.clamp(50, 2000);
state.speed_ms.store(clamped, Ordering::Relaxed);
}

View File

@@ -1,4 +1,3 @@
pub mod aura_animator;
pub mod setup_anime;
pub mod setup_aura;
pub mod setup_fan_curve_custom;
@@ -6,7 +5,6 @@ pub mod setup_fans;
pub mod setup_screenpad;
pub mod setup_slash;
pub mod setup_status;
pub mod setup_supergfx;
pub mod setup_system;
use std::sync::{Arc, Mutex};
@@ -24,7 +22,6 @@ 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_supergfx::setup_supergfx;
use crate::ui::setup_system::{setup_system_page, setup_system_page_callbacks};
use crate::{AppSettingsPageData, MainWindow};
@@ -116,20 +113,7 @@ pub fn setup_window(
available.contains(&"xyz.ljones.Aura".to_string()),
available.contains(&"xyz.ljones.Anime".to_string()),
available.contains(&"xyz.ljones.Slash".to_string()),
// Supergfx check
{
if let Ok(conn) = zbus::blocking::Connection::system() {
zbus::blocking::fdo::DBusProxy::new(&conn)
.ok()
.and_then(|p| {
p.name_has_owner("org.supergfxctl.Daemon".try_into().ok()?)
.ok()
})
.unwrap_or(false)
} else {
false
}
},
false,
// Screenpad check (Backlight interface)
available.contains(&"xyz.ljones.Backlight".to_string()),
available.contains(&"xyz.ljones.FanCurves".to_string()),
@@ -186,9 +170,7 @@ pub fn setup_window(
setup_slash(&ui, config.clone());
}
// Always try to setup supergfx if detected above, but for simplicity here we assume if sidebar has it (re-check or just run)
// We didn't capture the boolean above. Let's just run it, it handles its own availability check internally via async proxy creation.
setup_supergfx(&ui, config.clone());
if available.contains(&"xyz.ljones.Backlight".to_string()) {
setup_screenpad(&ui, config.clone());
@@ -237,22 +219,6 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc<Mutex<Config>>) {
});
// Granular notification toggles
let config_copy = config.clone();
global.on_set_notify_gfx_switch(move |enable| {
if let Ok(mut lock) = config_copy.try_lock() {
lock.notifications.receive_notify_gfx = enable;
lock.write();
}
});
let config_copy = config.clone();
global.on_set_notify_gfx_status(move |enable| {
if let Ok(mut lock) = config_copy.try_lock() {
lock.notifications.receive_notify_gfx_status = enable;
lock.write();
}
});
let config_copy = config.clone();
global.on_set_notify_platform_profile(move |enable| {
if let Ok(mut lock) = config_copy.try_lock() {
@@ -267,8 +233,6 @@ pub fn setup_app_settings_page(ui: &MainWindow, config: Arc<Mutex<Config>>) {
global.set_startup_in_background(lock.startup_in_background);
global.set_enable_tray_icon(lock.enable_tray_icon);
global.set_notifications_enabled(lock.notifications.enabled);
global.set_notify_gfx_switch(lock.notifications.receive_notify_gfx);
global.set_notify_gfx_status(lock.notifications.receive_notify_gfx_status);
global.set_notify_platform_profile(lock.notifications.receive_notify_platform_profile);
}
}

View File

@@ -1,15 +1,13 @@
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};
use crate::config::Config;
use crate::ui::aura_animator::{
set_animation_mode, set_animation_speed, start_animator, AnimationMode, AnimatorState,
};
use crate::ui::show_toast;
use crate::{
set_ui_callbacks, set_ui_props_async, AuraPageData, MainWindow, PowerZones as SlintPowerZones,
@@ -126,17 +124,15 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc<Mutex<Config>>) {
.ok();
}
// Create animator state (shared across callbacks)
let animator_state = Arc::new(AnimatorState::default());
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());
let handle_for_anim = handle.clone();
let animator_state_clone = animator_state.clone();
// 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();
@@ -163,23 +159,69 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc<Mutex<Config>>) {
.global::<AuraPageData>()
.set_soft_animation_available(true);
// Start the animator thread
start_animator(animator_state_clone.clone(), handle_for_anim.clone());
// Connect mode callback
let state_for_mode = animator_state_clone.clone();
// 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| {
set_animation_mode(&state_for_mode, AnimationMode::from(mode));
});
let aura_inner = aura_mode.clone();
let handle = match handle_weak.upgrade() {
Some(h) => h,
None => return,
};
// Connect speed callback
let state_for_speed = animator_state_clone.clone();
handle
.global::<AuraPageData>()
.on_cb_soft_animation_speed(move |speed| {
set_animation_speed(&state_for_speed, speed as u32);
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}");
}
}
});
});
}
})
@@ -204,12 +246,79 @@ pub fn setup_aura_page(ui: &MainWindow, _states: Arc<Mutex<Config>>) {
"Setting keyboard LEDmode failed"
);
set_ui_callbacks!(handle,
AuraPageData(.into()),
proxy_copy.led_mode_data(.into()),
"Keyboard LED mode set to {:?}",
"Setting keyboard LED mode 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}");
}
}
});
}
});
// set_ui_callbacks!(handle,
// AuraPageData(.clone().into()),

View File

@@ -1,102 +0,0 @@
use crate::config::Config;
use crate::ui::show_toast;
use crate::{MainWindow, SupergfxPageData};
use slint::{ComponentHandle, Model, SharedString, VecModel};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use zbus::proxy;
#[proxy(
interface = "org.supergfxctl.Daemon",
default_service = "org.supergfxctl.Daemon",
default_path = "/org/supergfxctl/Gfx"
)]
trait Supergfx {
fn supported(&self) -> zbus::Result<Vec<String>>;
fn mode(&self) -> zbus::Result<String>;
fn set_mode(&self, mode: &str) -> zbus::Result<()>;
fn vendor(&self) -> zbus::Result<String>;
}
pub fn setup_supergfx(ui: &MainWindow, _config: Arc<Mutex<Config>>) {
let ui_weak = ui.as_weak();
tokio::spawn(async move {
let conn = match zbus::Connection::system().await {
Ok(c) => c,
Err(e) => {
log::warn!("Failed to connect to system bus: {}", e);
return;
}
};
let proxy = match SupergfxProxy::new(&conn).await {
Ok(p) => p,
Err(e) => {
log::warn!("Failed to create Supergfx proxy: {}", e);
return;
}
};
// Register Callbacks on UI Thread
{
let proxy_copy = proxy.clone();
let ui_weak_copy = ui_weak.clone();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
let handle_copy = ui_weak_copy.clone();
ui.global::<SupergfxPageData>()
.on_set_mode(move |mode_str| {
let proxy = proxy_copy.clone();
let handle = handle_copy.clone();
tokio::spawn(async move {
show_toast(
format!("Switching to {}. Logout required.", mode_str).into(),
"Failed to set mode".into(),
handle,
proxy.set_mode(&mode_str).await,
);
});
});
});
}
// Fetch Initial State
// Vendor
if let Ok(vendor) = proxy.vendor().await {
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
ui.global::<SupergfxPageData>().set_vendor(vendor.into())
});
}
// Supported Modes
if let Ok(supported) = proxy.supported().await {
let modes: Vec<SharedString> = supported
.iter()
.map(|s| SharedString::from(s.as_str()))
.collect();
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
let mode_model = Rc::new(VecModel::from(modes));
ui.global::<SupergfxPageData>()
.set_supported_modes(mode_model.into())
});
}
// Current Mode
if let Ok(mode) = proxy.mode().await {
let _ = ui_weak.upgrade_in_event_loop(move |ui| {
let g = ui.global::<SupergfxPageData>();
g.set_current_mode(mode.clone().into());
// Update selection index
let model = g.get_supported_modes();
for (i, m) in model.iter().enumerate() {
if m == mode.as_str() {
g.set_selected_index(i as i32);
break;
}
}
});
}
// No signal monitoring implemented as supergfxctl state changes usually require user action/logout
});
}

View File

@@ -1,9 +1,28 @@
import { SystemDropdown, RogItem, SystemToggle, SystemToggleVert } from "../widgets/common.slint";
import {
SystemDropdown,
RogItem,
SystemToggle,
SystemToggleVert,
} from "../widgets/common.slint";
import { Button, ComboBox, VerticalBox, GroupBox } from "std-widgets.slint";
import { RogPalette } from "../themes/rog_theme.slint";
import { StyleMetrics, Slider, HorizontalBox, TextEdit, SpinBox, LineEdit, ScrollView } from "std-widgets.slint";
import {
StyleMetrics,
Slider,
HorizontalBox,
TextEdit,
SpinBox,
LineEdit,
ScrollView,
} from "std-widgets.slint";
import { ColourSlider } from "../widgets/colour_picker.slint";
import { AuraPageData, AuraDevType, PowerZones, LaptopAuraPower, AuraEffect } from "../types/aura_types.slint";
import {
AuraPageData,
AuraDevType,
PowerZones,
LaptopAuraPower,
AuraEffect,
} from "../types/aura_types.slint";
import { AuraPowerGroup, AuraPowerGroupOld } from "../widgets/aura_power.slint";
export component PageAura inherits Rectangle {
@@ -190,22 +209,21 @@ export component PageAura inherits Rectangle {
VerticalLayout {
padding: 10px;
spacing: 8px;
Text {
text: @tr("Software Animation (Static-only keyboards)");
font-size: 14px;
font-weight: 600;
color: RogPalette.accent;
}
HorizontalLayout {
spacing: 20px;
VerticalLayout {
Text {
text: @tr("Animation Mode");
color: RogPalette.text-secondary;
}
ComboBox {
current_index <=> AuraPageData.soft_animation_mode;
current_value: AuraPageData.soft_animation_modes[self.current-index];
@@ -215,22 +233,6 @@ export component PageAura inherits Rectangle {
}
}
}
VerticalLayout {
horizontal-stretch: 1;
Text {
text: @tr("Speed: ") + Math.round(AuraPageData.soft_animation_speed) + "ms";
color: RogPalette.text-secondary;
}
Slider {
minimum: 150;
maximum: 1000;
value <=> AuraPageData.soft_animation_speed;
released => {
AuraPageData.cb_soft_animation_speed(Math.round(AuraPageData.soft_animation_speed));
}
}
}
}
}
}

View File

@@ -179,6 +179,8 @@ export global AuraPageData {
@tr("Animation mode" => "None"),
@tr("Animation mode" => "Rainbow"),
@tr("Animation mode" => "Color Cycle"),
@tr("Animation mode" => "Breathe"),
@tr("Animation mode" => "Pulse"),
];
in-out property <int> soft_animation_mode: 0;
in-out property <float> soft_animation_speed: 200; // ms between updates

View File

@@ -84,6 +84,21 @@ pub trait Aura {
/// SupportedPowerZones property
#[zbus(property)]
fn supported_power_zones(&self) -> zbus::Result<Vec<PowerZones>>;
/// Start a software-controlled animation in the daemon
/// `mode_json` is a JSON-serialized AnimationMode
fn start_animation(&self, mode_json: String) -> zbus::Result<()>;
/// Stop any running animation
fn stop_animation(&self) -> zbus::Result<()>;
/// AnimationRunning property - check if animation is active
#[zbus(property)]
fn animation_running(&self) -> zbus::Result<bool>;
/// AnimationMode property - get current animation mode as JSON
#[zbus(property)]
fn animation_mode(&self) -> zbus::Result<String>;
}
pub struct AuraProxyPerkey<'a>(AuraProxyBlocking<'a>);