use std::sync::{Arc, Mutex}; use log::{debug, error, info}; use rog_aura::keyboard::LaptopAuraPower; use rog_aura::{AuraDeviceType, PowerZones}; use rog_dbus::zbus_aura::AuraProxy; use slint::{ComponentHandle, Model, RgbaColor, SharedString}; use crate::config::Config; use crate::ui::show_toast; use crate::{ set_ui_callbacks, set_ui_props_async, AuraPageData, MainWindow, PowerZones as SlintPowerZones, }; fn decode_hex(s: &str) -> RgbaColor { let s = s.trim_start_matches('#'); if s.len() < 6 { return RgbaColor { alpha: 255, red: 0, green: 0, blue: 0, }; } let c: Vec = (0..s.len()) .step_by(2) .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap_or(164)) .collect(); RgbaColor { alpha: 255, red: *c.first().unwrap_or(&255), green: *c.get(1).unwrap_or(&128), blue: *c.get(2).unwrap_or(&32), } } async fn find_aura_iface() -> Result, Box> { let conn = zbus::Connection::system().await?; let mgr = zbus::fdo::ObjectManagerProxy::new(&conn, "xyz.ljones.Asusd", "/").await?; let objs = mgr.get_managed_objects().await?; let mut paths: Vec = objs .iter() .filter(|(_, ifaces)| ifaces.keys().any(|k| k.as_str() == "xyz.ljones.Aura")) .map(|(p, _)| p.clone()) .collect(); if paths.len() > 1 { log::debug!("Multiple aura devices: {paths:?}"); } let path = paths.pop().ok_or("No Aura interface")?; AuraProxy::builder(&conn) .path(path)? .destination("xyz.ljones.Asusd")? .build() .await .map_err(Into::into) } pub async fn prefetch_supported_basic_modes() -> Option> { let proxy = find_aura_iface().await.ok()?; let modes = proxy.supported_basic_modes().await.ok()?; Some(modes.iter().map(|n| (*n).into()).collect()) } pub fn setup_aura_page( ui: &MainWindow, _states: Arc>, prefetched_supported: Option>, ) { let g = ui.global::(); g.on_cb_hex_from_colour(|c| { format!("#{:02X}{:02X}{:02X}", c.red(), c.green(), c.blue()).into() }); g.on_cb_hex_to_colour(|s| decode_hex(s.as_str()).into()); let handle = ui.as_weak(); tokio::spawn(async move { let Ok(aura) = find_aura_iface().await else { info!("No aura interfaces"); return Ok::<(), zbus::Error>(()); }; set_ui_props_async!(handle, aura, AuraPageData, brightness); set_ui_props_async!(handle, aura, AuraPageData, led_power); set_ui_props_async!(handle, aura, AuraPageData, device_type); let modes_vec: Vec = match prefetched_supported { Some(p) => p, None => aura .supported_basic_modes() .await .ok() .map(|m| m.iter().map(|n| (*n).into()).collect()) .unwrap_or_default(), }; // Restore saved mode, colours, zone, speed, direction from asusd (persisted to disk). // Use effect.mode as single source — avoid led_mode() which can fail (try_lock). let restore = aura.led_mode_data().await.ok(); let raw_mode: Option = restore.as_ref().map(|d| d.mode.into()); let d_slint = restore.map(|d| d.into()); handle .upgrade_in_event_loop(move |h| { let names = h.global::().get_mode_names(); let mut raws = Vec::new(); let mut mode_names = Vec::new(); for (i, name) in names.iter().enumerate() { let raw = i as i32; if modes_vec.contains(&raw) && i != 9 { raws.push(raw); mode_names.push(name.clone()); } } h.global::() .set_supported_basic_modes(raws.as_slice().into()); h.global::() .set_available_mode_names(mode_names.as_slice().into()); if let Some(d) = d_slint { h.global::().invoke_update_led_mode_data(d); if let Some(cm) = raw_mode { let idx = raws.iter().position(|&r| r == cm).unwrap_or(0) as i32; h.global::().set_current_available_mode(idx); } h.invoke_external_colour_change(); } }) .map_err(|e| error!("{e}")) .ok(); if let Ok(mut pow3r) = aura.supported_power_zones().await { let dev = aura .device_type() .await .unwrap_or(AuraDeviceType::LaptopKeyboard2021); handle .upgrade_in_event_loop(move |handle| { let names: Vec = handle .global::() .get_power_zone_names() .iter() .collect(); if dev.is_old_laptop() { if pow3r.contains(&PowerZones::Keyboard) && pow3r.contains(&PowerZones::Lightbar) { pow3r.push(PowerZones::KeyboardAndLightbar); } let n: Vec = pow3r.iter().map(|z| names[(*z) as usize].clone()).collect(); handle .global::() .set_power_zone_names_old(n.as_slice().into()); } else { let p: Vec = pow3r.iter().map(|z| (*z).into()).collect(); handle .global::() .set_supported_power_zones(p.as_slice().into()); } }) .ok(); } let proxy = aura.clone(); let weak = handle.clone(); handle .upgrade_in_event_loop(move |h| { set_ui_callbacks!(h, AuraPageData(.into()), proxy.brightness(.into()), "Brightness set to {}", "Brightness failed" ); let p = proxy.clone(); let w = weak.clone(); h.global::().on_apply_led_mode_data(move || { let Some(ui) = w.upgrade() else { return }; let slint_effect = ui.global::().get_led_mode_data(); let raw: rog_aura::AuraEffect = slint_effect.into(); let pp = p.clone(); let t = w.clone(); tokio::spawn(async move { let r = pp.set_led_mode_data(raw).await; show_toast("LED mode applied".into(), "LED mode failed".into(), t, r); }); }); h.invoke_external_colour_change(); }) .ok(); let weak_power = handle.clone(); let proxy_power = aura.clone(); handle .upgrade_in_event_loop(|h| { h.global::().on_cb_led_power(move |power| { let w = weak_power.clone(); let p = proxy_power.clone(); let pw: LaptopAuraPower = power.into(); tokio::spawn(async move { show_toast( "Aura power updated".into(), "Aura power failed".into(), w, p.set_led_power(pw).await, ); }); }); }) .map_err(|e| error!("{e}")) .ok(); let stream_handle = handle.clone(); let aura_stream = aura.clone(); tokio::spawn(async move { use futures_util::StreamExt; let mut stream = aura_stream.receive_led_mode_data_changed().await; while let Some(e) = stream.next().await { if let Ok(out) = e.get().await { let raw: i32 = out.mode.into(); let data = out.into(); stream_handle .upgrade_in_event_loop(move |h| { h.global::().invoke_update_led_mode_data(data); let supported: Vec = h .global::() .get_supported_basic_modes() .iter() .collect(); let idx = supported.iter().position(|&x| x == raw).unwrap_or(0) as i32; h.global::().set_current_available_mode(idx); h.invoke_external_colour_change(); }) .map_err(|e| error!("{e}")) .ok(); } } }); debug!("Aura setup done"); Ok(()) }); }