use anyhow::{Context, Result};
use clap::{Args, Subcommand};
use openlogi_hid::{
BacklightChange, BacklightControls, BacklightMode, BacklightState, BacklightStatus, DeviceRoute,
};
use crate::cmd::api::Switch;
use crate::cmd::diag::select_device;
const BACKLIGHT_FEATURE: u16 = 0x1982;
#[derive(Debug, Args)]
pub struct BacklightArgs {
#[arg(long, value_name = "NAME", global = true)]
pub device: Option<String>,
#[command(subcommand)]
pub action: Option<BacklightAction>,
}
#[derive(Debug, Subcommand)]
pub enum BacklightAction {
Status,
Off,
On,
Auto {
#[arg(value_enum)]
set: Switch,
},
Level {
#[arg(value_name = "LEVEL")]
level: u8,
},
BatterySaving {
#[arg(value_enum)]
set: Switch,
},
}
impl BacklightAction {
fn change(&self) -> Option<BacklightChange> {
match *self {
Self::Status => None,
Self::Off => Some(BacklightChange::Enabled(false)),
Self::On => Some(BacklightChange::Enabled(true)),
Self::Auto { set } => Some(BacklightChange::Automatic(set.is_on())),
Self::Level { level } => Some(BacklightChange::Level(level)),
Self::BatterySaving { set } => Some(BacklightChange::PowerSave(set.is_on())),
}
}
}
pub async fn run(args: BacklightArgs) -> Result<()> {
let (route, name) = select_device(args.device.as_deref(), &[BACKLIGHT_FEATURE]).await?;
println!("device: {name} ({route})");
let before = read_state(&route).await?;
print_state("current", before);
let Some(change) = args.action.unwrap_or(BacklightAction::Status).change() else {
println!(" offers: {}", offers(before.controls));
return Ok(());
};
if before.mode == BacklightMode::TemporaryManual && keeps_the_mode(change) {
println!(
" note: the level came from the keyboard's backlight keys, a mode software cannot write back — it returns to automatic (ambient-light sensor)"
);
}
let after = openlogi_hid::update_backlight(&route, change)
.await
.with_context(|| format!("set {change}"))?;
print_state("read-back", after);
println!("✓ {change} — persisted to the keyboard, survives reconnect and power cycle");
Ok(())
}
fn keeps_the_mode(change: BacklightChange) -> bool {
matches!(
change,
BacklightChange::Enabled(_) | BacklightChange::PowerSave(_)
)
}
async fn read_state(route: &DeviceRoute) -> Result<BacklightState> {
openlogi_hid::get_backlight(route)
.await
.context("read backlight state")
}
fn print_state(label: &str, state: BacklightState) {
println!(
" {label}: enabled={} mode={} status={} level={}/{} battery-saving={}",
state.enabled,
mode_label(state.mode),
status_label(state.status),
state.current_level,
state.nb_levels,
state.power_save,
);
}
fn offers(controls: BacklightControls) -> String {
let offered: Vec<&str> = [
(controls.automatic, "auto"),
(controls.level, "level"),
(controls.power_save, "battery-saving"),
]
.into_iter()
.filter_map(|(offered, name)| offered.then_some(name))
.collect();
if offered.is_empty() {
"on/off only".to_string()
} else {
format!("on/off, {}", offered.join(", "))
}
}
fn mode_label(mode: BacklightMode) -> &'static str {
match mode {
BacklightMode::None => "none",
BacklightMode::Automatic => "automatic (ambient-light sensor)",
BacklightMode::TemporaryManual => "temporary manual (backlight keys)",
BacklightMode::PermanentManual => "permanent manual (software)",
}
}
fn status_label(status: BacklightStatus) -> &'static str {
match status {
BacklightStatus::DisabledBySoftware => "off (disabled by software)",
BacklightStatus::DisabledByCriticalBattery => "off (critical battery)",
BacklightStatus::AlsAutomatic => "on (following ambient light)",
BacklightStatus::AlsSaturated => "off (ambient light saturated)",
BacklightStatus::TemporaryManual => "on (level from backlight keys)",
BacklightStatus::PermanentManual => "on (level from software)",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_action_writes_its_own_change() {
assert_eq!(BacklightAction::Status.change(), None);
assert_eq!(
BacklightAction::Off.change(),
Some(BacklightChange::Enabled(false))
);
assert_eq!(
BacklightAction::Auto { set: Switch::Off }.change(),
Some(BacklightChange::Automatic(false))
);
assert_eq!(
BacklightAction::Level { level: 3 }.change(),
Some(BacklightChange::Level(3))
);
assert_eq!(
BacklightAction::BatterySaving { set: Switch::On }.change(),
Some(BacklightChange::PowerSave(true))
);
}
#[test]
fn status_lists_only_what_the_keyboard_offers() {
assert_eq!(offers(BacklightControls::default()), "on/off only");
let level_only = BacklightControls {
level: true,
..BacklightControls::default()
};
assert_eq!(offers(level_only), "on/off, level");
let all = BacklightControls {
automatic: true,
level: true,
power_save: true,
};
assert_eq!(offers(all), "on/off, auto, level, battery-saving");
}
#[test]
fn only_a_change_that_keeps_the_mode_warns_about_the_backlight_keys() {
assert!(keeps_the_mode(BacklightChange::Enabled(true)));
assert!(keeps_the_mode(BacklightChange::PowerSave(false)));
assert!(!keeps_the_mode(BacklightChange::Automatic(false)));
assert!(!keeps_the_mode(BacklightChange::Level(2)));
}
}