Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions asusd/src/aura_slash/trait_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ impl SlashZbus {
}

#[zbus(property)]
async fn mode(&self) -> zbus::fdo::Result<u8> {
async fn mode(&self) -> zbus::fdo::Result<SlashMode> {
let config = self.0.lock_config().await;
Ok(config.display_interval)
Ok(config.display_mode)
}

/// Set interval between slash animations (0-255)
Expand Down
4 changes: 4 additions & 0 deletions rog-control-center/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ pub struct Config {
pub start_fullscreen: bool,
pub fullscreen_width: u32,
pub fullscreen_height: u32,
#[serde(default)]
pub language: String,
// This field must be last
pub notifications: EnabledNotifications,
}
Expand All @@ -40,6 +42,7 @@ impl Default for Config {
start_fullscreen: false,
fullscreen_width: 1920,
fullscreen_height: 1080,
language: String::new(),
notifications: EnabledNotifications::default(),
ac_command: String::new(),
bat_command: String::new(),
Expand Down Expand Up @@ -100,6 +103,7 @@ impl From<Config461> for Config {
start_fullscreen: false,
fullscreen_width: 1920,
fullscreen_height: 1080,
language: String::new(),
notifications: c.enabled_notifications,
}
}
Expand Down
32 changes: 27 additions & 5 deletions rog-control-center/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,16 @@ async fn main() -> Result<()> {
}
}

// Try to open a proxy and check for app state first
{
// Single-instance guard. Skipped when this binary re-spawned itself for a
// "Reload Window" — the child carries ROGCC_NO_SINGLE_INSTANCE so it doesn't
// see the still-registered parent and exit; the parent quits right after
// spawning, freeing the name for future reloads. The bypass is one-shot:
// consume it below so a reload triggered from *this* process can't pass the
// skip on to its own child (otherwise the guard stays off forever).
let skip_single_instance = std::env::var_os("ROGCC_NO_SINGLE_INSTANCE").is_some();
std::env::remove_var("ROGCC_NO_SINGLE_INSTANCE");
if !skip_single_instance {
// Try to open a proxy and check for app state first
let user_con = zbus::blocking::Connection::session()?;
if let Ok(proxy) = ROGCCZbusProxyBlocking::new(&user_con) {
if let Ok(state) = proxy.state() {
Expand Down Expand Up @@ -92,6 +100,18 @@ async fn main() -> Result<()> {
// return Ok(());
}

// Apply the configured UI language through env vars before Runtime::new
// (they're process-global). "Reload Window" restarts the process, which
// re-reads config.language here so gettext resolves @tr() in the chosen
// locale at init_translations — no setlocale / unsafe / libc needed.
let startup_language = Config::new().load().language;
if !startup_language.is_empty() {
let locale = format!("{startup_language}.UTF-8");
env::set_var("LANG", &locale);
env::set_var("LC_ALL", &locale);
env::set_var("LANGUAGE", &startup_language);
}

Comment thread
NB-Group marked this conversation as resolved.
// start tokio
let rt = Runtime::new().expect("Unable to create Runtime");
// Enter the runtime so that `tokio::spawn` is available immediately.
Expand Down Expand Up @@ -171,6 +191,9 @@ async fn main() -> Result<()> {
};
let config = Arc::new(Mutex::new(config));

// setlocale + LANG/LC_ALL env were applied before Runtime::new (see above);
// gettext picks them up at init_translations below.

// GPU power status channel: written by the dGPU status monitor in
// notify.rs, read by the tray to color its icon
let (gpu_status_tx, gpu_status_rx) =
Expand All @@ -185,11 +208,10 @@ async fn main() -> Result<()> {
}

if std::env::var("RUST_TRANSLATIONS").is_ok() {
// don't care about content
log::debug!("---- Using local-dir translations");
log::debug!("Using system-installed translations");
slint::init_translations!("/usr/share/locale/");
} else {
log::debug!("Using system installed translations");
log::debug!("Using local translations");
slint::init_translations!(concat!(env!("CARGO_MANIFEST_DIR"), "/translations/"));
}

Expand Down
117 changes: 117 additions & 0 deletions rog-control-center/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ pub fn setup_window(
let ui = MainWindow::new().expect("Couldn't create main window");
// propagate TUF flag to the UI so the sidebar can swap logo branding
ui.set_is_tuf(is_tuf);
ui.set_app_version(env!("CARGO_PKG_VERSION").into());
if let Err(e) = ui.window().show() {
warn!("Couldn't show main window: {e:?}");
}
Expand Down Expand Up @@ -202,6 +203,62 @@ fn ui_shortcut_status(status: ShortcutStatus) -> GlobalShortcutStatus {
}
}

/// Locale codes that have a translation on disk: the source `translations/`
/// tree (every subdir is ours) plus any installed under `/usr/share/locale`
/// that actually ships our catalog. Sorted + deduped, with an "en" fallback so
/// the picker is never empty. The locale *list* is automatic; the native
/// display names live in `language_display_name` (add a line there when a new
/// translation lands — until then it shows the raw code).
fn available_languages() -> Vec<SharedString> {
let mut set: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
// English is the source language (no .mo needed) — always present, so a
// fresh config never lands on a translation by default.
set.insert("en".to_owned());
let dev = concat!(env!("CARGO_MANIFEST_DIR"), "/translations");
for dir in [dev, "/usr/share/locale"] {
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
// /usr/share/locale holds every app's locales, so only count
// dirs carrying our catalog; the source tree is all ours.
let ours = dir == dev || path.join("LC_MESSAGES/rog-control-center.mo").exists();
if ours {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
set.insert(name.to_string());
}
}
}
}
}
let mut v: Vec<SharedString> = set.into_iter().map(SharedString::from).collect();
if v.is_empty() {
v.push(SharedString::from("en"));
}
v
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// Native display name for a locale code — language pickers conventionally
/// show each language in its own tongue. Unknown codes fall back to themselves
/// so a newly added translation still shows up (as its code) until named here.
fn language_display_name(code: &str) -> SharedString {
let name = match code {
"en" => "English",
"zh_CN" => "简体中文",
"fr" => "Français",
"it" => "Italiano",
"ru" => "Русский",
"tr" => "Türkçe",
"uk_UA" => "Українська",
"pt_BR" => "Português (Brasil)",
"az" => "Azərbaycanca",
other => other,
};
SharedString::from(name)
}
Comment thread
NB-Group marked this conversation as resolved.

pub fn setup_app_settings_page(
ui: &MainWindow,
config: Arc<Mutex<Config>>,
Expand Down Expand Up @@ -365,4 +422,64 @@ pub fn setup_app_settings_page(
});
});
}

// Discover shipped translations at startup so the picker lists every
// language without a hardcoded array.
let codes = available_languages();
let configured_language = config
.try_lock()
.map(|lock| lock.language.clone())
.unwrap_or_default();
// Match the persisted language; fall back to "en" (source language), then
// index 0 — so a stale config like the old "en_US" still lands on English.
let current_idx = codes
.iter()
.position(|l| l.as_str() == configured_language.as_str())
.or_else(|| codes.iter().position(|l| l.as_str() == "en"))
.unwrap_or(0) as i32;
// The picker shows each language in its own name (standard for language
// selectors); the raw code is what gets persisted, so keep both in lockstep.
let display: Vec<SharedString> = codes
.iter()
.map(|c| language_display_name(c.as_str()))
.collect();
global.set_available_languages(slint::ModelRc::new(slint::VecModel::from(display)));
global.set_current_language(current_idx);

let config_copy = config.clone();
global.on_cb_change_language(move |index: i32| {
if let Some(code) = codes.get(index as usize) {
if let Ok(mut lock) = config_copy.try_lock() {
lock.language = code.to_string();
lock.write();
}
log::info!("Language changed to {code}; reload to apply");
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Reload Window: spawn a fresh instance flagged to skip the single-instance
// guard (ROGCC_NO_SINGLE_INSTANCE), then quit this one. spawn+quit is
// reliable where exec() was not: the old DBus name is released on quit and
// the new image never races the check. The child re-reads config.language
// and re-resolves @tr() in the chosen locale.
global.on_cb_reload_window(move || {
let exe = match std::env::current_exe() {
Ok(e) => e,
Err(e) => {
log::error!("reload: cannot resolve current exe: {e}");
return;
}
};
log::info!("reload: spawning {:?}", exe);
match std::process::Command::new(exe)
.env("ROGCC_NO_SINGLE_INSTANCE", "1")
.spawn()
{
Ok(_) => {
slint::quit_event_loop()
.unwrap_or_else(|e| log::error!("reload: quit_event_loop: {e}"));
}
Err(e) => log::error!("reload: spawn failed: {e}"),
}
});
Comment thread
NB-Group marked this conversation as resolved.
}
13 changes: 13 additions & 0 deletions rog-control-center/src/ui/setup_aura.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,19 @@ pub fn setup_aura_page(
show_toast("LED mode applied".into(), "LED mode failed".into(), t, r);
});
});

// Speed slider: stamp the new speed onto the active effect,
// mirror it in the UI, then run the existing apply path (which
// also pushes to hardware and toasts) so the write logic stays
// in one place.
let w_speed = weak.clone();
h.global::<AuraPageData>().on_cb_speed(move |speed| {
let Some(ui) = w_speed.upgrade() else { return };
let mut data = ui.global::<AuraPageData>().get_led_mode_data();
data.speed = speed;
ui.global::<AuraPageData>().set_led_mode_data(data);
ui.global::<AuraPageData>().invoke_apply_led_mode_data();
});
h.invoke_external_colour_change();
})
.ok();
Expand Down
27 changes: 25 additions & 2 deletions rog-control-center/src/ui/setup_slash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,29 @@ pub fn setup_slash_page(ui: &MainWindow, _states: Arc<Mutex<Config>>) {
set_ui_props_async!(handle, slash, SlashPageData, show_battery_warning);
set_ui_props_async!(handle, slash, SlashPageData, show_on_lid_closed);

if let Ok(mode) = slash.mode().await {
let idx = slash_mode_to_index(mode);
// Read the persisted mode, retrying briefly — a single read right
// after (re)start can race with asusd and fail, which would leave
// the UI stuck on the Static default. The mode-changed listener
// below only fires on *changes*, so it won't recover an initial
// read failure on its own.
let mut mode: Option<SlashMode> = None;
for attempt in 0..5u32 {
match slash.mode().await {
Ok(m) => {
mode = Some(m);
break;
}
Err(e) => {
log::warn!("slash mode read attempt {} failed: {e}", attempt + 1);
if attempt < 4 {
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
}
}
}
}
Comment thread
NB-Group marked this conversation as resolved.
if let Some(m) = mode {
let idx = slash_mode_to_index(m);
log::info!("slash mode at startup: {:?} (index {})", m, idx);
let choices = slash_modes();
handle
.upgrade_in_event_loop(move |handle| {
Expand All @@ -64,6 +85,8 @@ pub fn setup_slash_page(ui: &MainWindow, _states: Arc<Mutex<Config>>) {
global.set_mode(idx);
})
.ok();
} else {
log::error!("slash mode unreadable after retries; UI will show Static");
}

handle
Expand Down
14 changes: 14 additions & 0 deletions rog-control-center/src/ui/setup_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ pub fn setup_system_page(
.set_dgpu_name(dgpu_model.into());
ui.global::<SystemPageData>().set_has_igpu(has_igpu);

// Real product name from DMI (e.g. "ROG Zephyrus G16 GU605MV")
let real_product_name = match std::fs::read_to_string("/sys/class/dmi/id/product_name") {
Ok(s) => s.trim().to_string(),
Err(e) => {
log::debug!("DMI product_name unreadable: {e}");
String::new()
}
};
if !real_product_name.is_empty() {
ui.global::<SystemPageData>().set_product_name(real_product_name.into());
}

if let Ok(sys_props) = platform
.supported_properties()
.map_err(|e| log::error!("Failed to get supported properties: {}", e))
Expand Down Expand Up @@ -147,6 +159,7 @@ pub fn setup_system_page(
let igpu_temp = rog_platform::gpu_pci::get_igpu_temp();
let (cpu_fan, gpu_fan, mid_fan) = rog_platform::platform::get_fan_rpms();
let cpu_freq = rog_platform::cpu::get_cpu_frequency_mhz();
let gpu_freq = rog_platform::gpu_pci::get_gpu_frequency_mhz().unwrap_or(-1.0);
let ram_usage = rog_platform::cpu::get_ram_usage_pct();
let gpu_usage = rog_platform::gpu_pci::get_gpu_usage_pct();
let igpu_usage = rog_platform::gpu_pci::get_igpu_usage_pct();
Expand Down Expand Up @@ -183,6 +196,7 @@ pub fn setup_system_page(
data.set_igpu_usage_val(igpu_usage);
data.set_ram_usage_val(ram_usage);
data.set_cpu_freq_mhz(cpu_freq);
data.set_gpu_freq_mhz(gpu_freq);
data.set_cpu_fan_rpm(cpu_fan);
data.set_gpu_fan_rpm(gpu_fan);
data.set_mid_fan_rpm(mid_fan);
Expand Down
Binary file not shown.
Loading